Claude Code for C# / .NET — ASP.NET Core, EF Core & xUnit Workflows
C# and .NET power the world's largest enterprise systems. This guide shows how to configure Claude Code for idiomatic .NET development — minimal APIs, Entity Framework Core migrations, xUnit testing, and clean architecture patterns — using CLAUDE.md and automated hooks.
CLAUDE.md Template for .NET Solutions
Add this to your solution root. Claude reads it on every startup and generates code that matches your conventions immediately.
# CLAUDE.md — .NET Solution
## Build & test commands
- dotnet build # Build all projects
- dotnet test # Run all tests
- dotnet test tests/MyApp.Tests -v minimal # Unit tests only
- dotnet ef migrations add <Name> --project Infrastructure --startup-project Api
- dotnet ef database update --project Infrastructure --startup-project Api
## Solution structure
- src/MyApp.Api — ASP.NET Core minimal API, middleware, endpoints
- src/MyApp.Domain — Entities, value objects, domain services, interfaces
- src/MyApp.Infrastructure — EF Core DbContext, repositories, external services
- tests/MyApp.Tests — xUnit + Moq + FluentAssertions
## C# conventions
- Target: net9.0
- Nullable reference types: ENABLED — never use ! or default! to suppress
- Required: use 'required' keyword for non-optional properties
- DTOs: use C# records with init setters
- No Newtonsoft.Json — use System.Text.Json with source generators
- Async: all I/O methods must be async. No .Result or .Wait()
- Logging: ILogger<T> via DI — no Console.WriteLine in production code
- Exceptions: custom exception types in Domain; catch only at boundary handlers
## EF Core
- Provider: Npgsql (PostgreSQL)
- Migrations must be zero-downtime (expand/contract for renames)
- No lazy loading — use explicit Include() or projection queries
- Use AsNoTracking() for read-only queries
Automated Hooks for .NET
# .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "dotnet build --no-restore 2>&1 | grep -E 'error|warning|Build' | head -30"
},
{
"type": "command",
"command": "dotnet test --no-build --filter Category=Unit 2>&1 | tail -25"
}
]
}
]
}
}
Use
--no-build on the test hook so you only pay compile cost once per save, not twice. The build hook runs first and catches compile errors; the test hook reuses the compiled output.C# / .NET Conventions for Claude Code
| Area | Convention to specify in CLAUDE.md | Why it matters |
|---|---|---|
| DTOs | Use records with required init: record CreateOrderRequest(required string ProductId, ...) | Immutable by default, structural equality, null-safety enforced by compiler |
| Error handling | Domain exceptions + Result<T,E> for expected failures; don't use exceptions for flow control | Prevents catching broad Exception in callers; forces explicit error handling |
| DI lifetime | Repositories = Scoped; HttpClient = Transient via IHttpClientFactory; caches = Singleton | Prevents captive dependency bug (singleton holding scoped service) |
| EF queries | Use .AsNoTracking() for reads; explicit .Include() for related data; no N+1 | Avoids silent change-tracking overhead on read-heavy endpoints |
| Cancellation | All async methods accept CancellationToken and pass it to EF/HttpClient calls | Enables request cancellation on client disconnect; required for resilient APIs |
| JSON | System.Text.Json with JsonSerializerContext source generation; snake_case property names | AOT-compatible, no reflection overhead, consistent API contract |
ASP.NET Core Endpoint Prompts
Minimal API POST endpoint with validation
# Prompt
"Add POST /api/v1/orders endpoint.
- Validate CreateOrderRequest with FluentValidation (ProductId required, Quantity 1-100)
- Use IOrderRepository injected via DI
- Return 201 with Location header and OrderId on success
- Return 422 ProblemDetails with field errors on validation failure
- Return 409 if order with same idempotency key already exists
- Include xUnit test with WebApplicationFactory covering all 3 status codes"
Entity Framework Core migration
# Prompt
"Add a new nullable 'ShippingAddress' column to Orders table.
Migration must be zero-downtime:
1. Migration 1: add nullable column
2. Application code updated to write new column (backfill on save)
3. Migration 2 (future): make NOT NULL after backfill complete
Generate only Migration 1 for now."
Background service (IHostedService)
# Prompt
"Add an IHostedService that processes orders from a channel.
- ExecuteAsync loop reads from Channel<Order> with CancellationToken
- Batches orders every 5s or when batch reaches 50 items
- Logs structured events with ILogger on success and failure
- Stops cleanly on app shutdown (CancellationToken.IsCancellationRequested)"
xUnit Testing Patterns
| Test type | Pattern | Claude Code prompt hint |
|---|---|---|
| Unit test (pure logic) | xUnit [Fact]/[Theory] + FluentAssertions | "Write unit tests for OrderService.CalculateTotal(). Cover: empty cart, single item, quantity discount at 10+, negative price guard." |
| Controller / endpoint test | WebApplicationFactory + HttpClient | "Write integration tests for POST /api/v1/orders using WebApplicationFactory. Mock IOrderRepository with Moq." |
| EF Core integration test | Testcontainers for PostgreSQL | "Write EF integration test using Testcontainers.PostgreSql. Migrate schema, seed fixture data, assert query returns expected rows." |
| Snapshot test | Verify.Xunit for JSON response shape | "Add Verify snapshot tests for the GetOrder response — any field addition/removal should require snapshot update approval." |
Clean Architecture with Claude Code
# Prompt for full slice
"Scaffold a full vertical slice for 'Create Product':
- Domain: Product entity + IProductRepository interface in Domain project
- Infrastructure: ProductRepository implementation + EF config in Infrastructure
- API: POST /api/v1/products minimal API endpoint in Api project
- Tests: unit test for domain logic + integration test for the endpoint
Follow the existing patterns in the codebase (nullable enabled, records for DTOs)."
Calculate Claude API costs for your .NET AI features
Try the Claude Cost Calculator →
Try the Claude Cost Calculator →
Integrating Claude API into ASP.NET Core
# Prompt
"Add a service that calls the Anthropic Claude API to summarize
customer feedback. Use HttpClient via IHttpClientFactory.
Add retry policy (Polly: 3 retries, exponential backoff, jitter).
Inject IClaudeSummaryService via DI with Scoped lifetime.
Include a unit test mocking the HttpClient."
// Claude generates — AnthropicService.cs
public class ClaudeSummaryService(IHttpClientFactory factory, ILogger<ClaudeSummaryService> logger) : IClaudeSummaryService
{
public async Task<string> SummariseFeedbackAsync(string text, CancellationToken ct = default)
{
var client = factory.CreateClient("Anthropic");
var payload = new
{
model = "claude-sonnet-4-6",
max_tokens = 256,
messages = new[] { new { role = "user", content = $"Summarise this customer feedback in 2 sentences:\n\n{text}" } }
};
var response = await client.PostAsJsonAsync("/v1/messages", payload, ct);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<AnthropicResponse>(cancellationToken: ct);
logger.LogInformation("Summarised feedback, input_tokens={In}", result!.Usage.InputTokens);
return result.Content[0].Text;
}
}
5 .NET Tips for Claude Code Users
- 1. Specify your exact framework version — net9.0 vs net8.0 vs netstandard2.1 changes which APIs are available. Include this in CLAUDE.md so Claude never generates APIs not available in your target.
- 2. Paste compiler error output, not just the error code — CS0246, CS8618 etc. are opaque; paste the full error line with context so Claude resolves it in one turn.
- 3. Use Solution Explorer structure in CLAUDE.md — tell Claude which project owns what (Domain has no dependencies on Infrastructure) so it never generates circular project references.
- 4. Ask for dotnet-format on every file change — add a PostToolUse hook for
dotnet format --include <file> 2>&1to enforce .editorconfig rules on every edit. - 5. Use source generators explicitly — mention in CLAUDE.md that you use
JsonSerializerContextsource generation; otherwise Claude defaults to reflection-based serialization that breaks in AOT-compiled binaries.
Related Guides
Java & Spring Boot → | Docker containers → | Database & EF migrations → | Testing workflows → | Hook patterns →