Esta é a Parte 2. Na Parte 1 vimos a configuração: o CLAUDE.md, o settings.json com regras de permissão, os modos de permissão e as skills. Aqui aplicamos tudo no kickoff de uma Minimal API em .NET 10 com testes, e fechamos com boas práticas, quando usar e quando evitar.
Tarefas API: Claude Code em um projeto .NET na prática
O projeto é a Tarefas API: uma Minimal API com lista de tarefas em memória. O repositório mostra cada etapa em um commit.

Passo 1: o repositório. Crie a solução e o git, e abra o Claude na raiz:
mkdir tarefas-api && cd tarefas-api
git init
dotnet new web -n Tarefas.Api -o src/Tarefas.Api
dotnet new xunit -n Tarefas.Api.Tests -o tests/Tarefas.Api.Tests
claudePasso 2: o CLAUDE.md. Rode /init para o rascunho e depois enxugue. O que ficou é curto, com comandos, estrutura, convenções e regras de fluxo (o ideal é menos de 200 linhas):
# Tarefas API
API mínima em ASP.NET Core (.NET 10) com lista de tarefas em
memória. Visão geral e como rodar: @README.md
## Comandos
- Build: `dotnet build`
- Testes: `dotnet test`
- Rodar a API: `dotnet run --project src/Tarefas.Api`
- Pacotes NuGet: sempre com
`--source https://api.nuget.org/v3/index.json`
## Estrutura
- `src/Tarefas.Api/Program.cs`: só composição (DI e rotas)
- `src/Tarefas.Api/TarefaEndpoints.cs`: todas as rotas
- `src/Tarefas.Api/TarefaRepositorio.cs`: armazenamento
- `tests/Tarefas.Api.Tests/`: testes de integração (xUnit)
## Convenções
- C# 14, `Nullable` habilitado, namespaces com escopo de arquivo.
- Linhas de código com no máximo 66 colunas.
- Rotas em `/tarefas`, nomes de domínio em português.
- Sem regra de negócio em `Program.cs`.
- Erros de validação voltam como `ValidationProblem` (400).
- Todo endpoint novo vem com pelo menos um teste de sucesso
e um de erro.
## Fluxo de trabalho
- Antes de mudar mais de um arquivo, proponha um plano curto.
- Rode `dotnet build` e `dotnet test` antes de dizer que
terminou.
- Não faça commit nem push sem eu pedir.
- Nunca leia nem escreva arquivos `.env`.Passo 3: permissões. O .claude/settings.json do projeto libera o que é seguro, pergunta nos commits e bloqueia o .env. (Dá para acrescentar a linha $schema do SchemaStore no topo, para autocompletar no editor.)
{
"permissions": {
"allow": [
"Bash(dotnet build *)",
"Bash(dotnet test *)",
"Bash(dotnet run *)",
"Bash(git status *)",
"Bash(git diff *)"
],
"ask": [
"Bash(git commit *)",
"Bash(git push *)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Bash(git push --force *)"
]
}
}Passo 4: skills. A primeira, novo-endpoint, descreve o roteiro de um endpoint novo. O $ARGUMENTS recebe o que você digita depois do nome (/novo-endpoint DELETE /tarefas/{id} remove uma tarefa):
---
name: novo-endpoint
description: >-
Adiciona um endpoint à Tarefas API seguindo as convenções do
projeto (rota em TarefaEndpoints.cs, teste em tests/). Use
quando pedirem um novo endpoint, rota ou recurso HTTP.
argument-hint: "[verbo] [rota] [descrição]"
allowed-tools:
- Bash(dotnet build *)
- Bash(dotnet test *)
---
# Novo endpoint
Pedido: $ARGUMENTS
Siga estes passos, nesta ordem:
1. Leia `src/Tarefas.Api/TarefaEndpoints.cs` e copie o estilo
das rotas existentes (grupo `/tarefas`, `Results.*`).
2. Se precisar de armazenamento novo, altere apenas
`TarefaRepositorio.cs`. Não coloque lógica em `Program.cs`.
3. Valide a entrada. Erro de validação devolve
`Results.ValidationProblem` (400).
4. Escreva os testes em `tests/Tarefas.Api.Tests/`: um de
sucesso e um de erro, usando `WebApplicationFactory<Program>`.
5. Rode `dotnet build` e `dotnet test`. Corrija até passar.
6. Responda com a lista de arquivos alterados e o resultado
dos testes. Não faça commit.
Linhas de código com no máximo 66 colunas.A segunda só roda quando você chama, por causa de disable-model-invocation: true. A linha com ! executa o comando antes de o Claude ler a skill e injeta a saída no texto:
---
name: pronto-para-commit
description: >-
Confere se as mudanças atuais estão prontas para commit e
sugere a mensagem.
disable-model-invocation: true
allowed-tools:
- Bash(dotnet build *)
- Bash(dotnet test *)
- Bash(git diff *)
- Bash(git status *)
---
# Pronto para commit?
## Arquivos alterados
!`git status --short`
## Instruções
1. Rode `dotnet build` e `dotnet test`.
2. Olhe `git diff` e aponte: linhas com mais de 66 colunas,
endpoint sem teste, segredo ou `.env` no diff.
3. Se tudo estiver certo, sugira uma mensagem de commit em uma
linha (imperativo, até 72 caracteres).
4. Não execute `git commit`. Quem decide é o desenvolvedor.Passo 5: o kickoff em modo plano. Com a configuração pronta, inicie com claude --permission-mode plan e faça o pedido com objetivo, restrições e critério de pronto:
Vamos começar a Tarefas API. Leia o CLAUDE.md e proponha um
plano antes de editar qualquer arquivo.
Objetivo: Minimal API em .NET 10 com tarefas em memória: criar,
obter por id, listar com filtro por concluída e concluir.
Restrições: sem banco; só pacotes de teste.
Pronto quando: dotnet build e dotnet test passarem.O plano esperado tem este formato (nomes dos arquivos vindos do CLAUDE.md): 1) tipos Tarefa e NovaTarefa; 2) TarefaRepositorio em memória; 3) rotas em TarefaEndpoints; 4) composição em Program.cs; 5) testes de integração; 6) build e testes. Você lê, corrige o que discordar (por exemplo, "Concluir deve devolver 404 se o id não existe") e aprova com Yes, manually approve edits na primeira vez, para ver cada edição.

Passo 6: o código resultante. Os tipos de domínio:
namespace Tarefas.Api;
public record Tarefa(int Id, string Titulo, bool Concluida);
public record NovaTarefa(string? Titulo);O repositório em memória, seguro para concorrência (ConcurrentDictionary e Interlocked):
using System.Collections.Concurrent;
namespace Tarefas.Api;
public class TarefaRepositorio
{
private readonly ConcurrentDictionary<int, Tarefa> _itens =
new();
private int _ultimoId;
public IReadOnlyList<Tarefa> Listar(bool? concluida = null) =>
_itens.Values
.Where(t => concluida is null
|| t.Concluida == concluida)
.OrderBy(t => t.Id)
.ToList();
public Tarefa? Obter(int id) => _itens.GetValueOrDefault(id);
public Tarefa Criar(string titulo)
{
var id = Interlocked.Increment(ref _ultimoId);
var tarefa = new Tarefa(id, titulo, false);
_itens[id] = tarefa;
return tarefa;
}
public Tarefa? Concluir(int id) =>
_itens.TryGetValue(id, out var atual)
? _itens[id] = atual with { Concluida = true }
: null;
}As rotas ficam em um método de extensão, como manda o CLAUDE.md, e a validação devolve ValidationProblem (400):
namespace Tarefas.Api;
public static class TarefaEndpoints
{
public static IEndpointRouteBuilder MapTarefas(
this IEndpointRouteBuilder app)
{
var grupo = app.MapGroup("/tarefas");
grupo.MapGet("/",
(TarefaRepositorio repo, bool? concluida) =>
repo.Listar(concluida));
grupo.MapGet("/{id:int}",
(int id, TarefaRepositorio repo) =>
repo.Obter(id) is { } t
? Results.Ok(t)
: Results.NotFound());
grupo.MapPost("/",
(NovaTarefa nova, TarefaRepositorio repo) =>
{
if (string.IsNullOrWhiteSpace(nova.Titulo))
{
return Results.ValidationProblem(
new Dictionary<string, string[]>
{
["titulo"] = ["Informe o título."]
});
}
var tarefa = repo.Criar(nova.Titulo.Trim());
return Results.Created(
$"/tarefas/{tarefa.Id}", tarefa);
});
grupo.MapPost("/{id:int}/concluir",
(int id, TarefaRepositorio repo) =>
repo.Concluir(id) is { } t
? Results.Ok(t)
: Results.NotFound());
return app;
}
}O Program.cs só compõe. A última linha expõe o Program para os testes de integração:
using Tarefas.Api;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<TarefaRepositorio>();
var app = builder.Build();
app.MapTarefas();
app.Run();
public partial class Program;Passo 7: os testes. O WebApplicationFactory<Program> sobe a API em memória, sem porta de rede. O pacote Microsoft.AspNetCore.Mvc.Testing entra no projeto de testes:
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Tarefas.Api;
namespace Tarefas.Api.Tests;
public class TarefasEndpointsTests(
WebApplicationFactory<Program> fabrica)
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _http = fabrica.CreateClient();
private async Task<Tarefa> CriarAsync(string titulo)
{
var resp = await _http.PostAsJsonAsync(
"/tarefas", new NovaTarefa(titulo));
resp.EnsureSuccessStatusCode();
return (await resp.Content.ReadFromJsonAsync<Tarefa>())!;
}
[Fact]
public async Task Criar_com_titulo_valido_retorna_201()
{
var resp = await _http.PostAsJsonAsync(
"/tarefas", new NovaTarefa("Ler a documentação"));
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
var tarefa = await resp.Content
.ReadFromJsonAsync<Tarefa>();
Assert.Equal("Ler a documentação", tarefa!.Titulo);
Assert.False(tarefa.Concluida);
Assert.Equal($"/tarefas/{tarefa.Id}",
resp.Headers.Location!.OriginalString);
}
[Fact]
public async Task Criar_com_titulo_vazio_retorna_400()
{
var resp = await _http.PostAsJsonAsync(
"/tarefas", new NovaTarefa(" "));
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
}
[Fact]
public async Task Obter_inexistente_retorna_404()
{
var resp = await _http.GetAsync("/tarefas/999999");
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
}
[Fact]
public async Task Concluir_marca_a_tarefa_como_concluida()
{
var criada = await CriarAsync("Rodar os testes");
var resp = await _http.PostAsync(
$"/tarefas/{criada.Id}/concluir", null);
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
var tarefa = await resp.Content
.ReadFromJsonAsync<Tarefa>();
Assert.True(tarefa!.Concluida);
}
[Fact]
public async Task Listar_filtra_por_concluida()
{
var aberta = await CriarAsync("Aberta");
var feita = await CriarAsync("Feita");
await _http.PostAsync(
$"/tarefas/{feita.Id}/concluir", null);
var abertas = await _http.GetFromJsonAsync<List<Tarefa>>(
"/tarefas?concluida=false");
Assert.Contains(abertas!, t => t.Id == aberta.Id);
Assert.DoesNotContain(abertas!, t => t.Id == feita.Id);
}
}Passo 8: build, testes e execução.
dotnet build
dotnet test
dotnet run --project src/Tarefas.ApiOs testes terminam com Aprovado! Com falha: 0, Aprovado: 5. Com a API no ar, o POST com título válido e o POST com título vazio respondem:
HTTP/1.1 201 Created
Location: /tarefas/1
{"id":1,"titulo":"Ler a documentação","concluida":false}
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{"title":"One or more validation errors occurred.",
"status":400,"errors":{"titulo":["Informe o título."]}}Passo 9: revisar e registrar. Chame /pronto-para-commit. A skill roda build e testes, aponta linhas longas e endpoint sem teste, e sugere a mensagem; o git commit continua sendo seu, protegido pela regra ask.
Boas práticas de CLAUDE.md, permissões e segredos
- CLAUDE.md enxuto: um arquivo gigante dilui as instruções. Meta de menos de 200 linhas; o que vale para só uma parte do código vai para regras por caminho em
.claude/rules/. Escreva o que o Claude não descobre sozinho: comandos, convenções e proibições. - Permissão específica, não ampla: libere comandos exatos.
"allow": ["Bash(*)"]
"allow": ["Bash(dotnet test *)", "Bash(git diff *)"]A primeira linha libera qualquer comando; a segunda, só o necessário. Regras deny valem em todos os modos.
- Segredos fora do repositório: nada de chave em CLAUDE.md ou .claude/settings.json (versionados). Use settings.local.json e CLAUDE.local.md, ambos no .gitignore. Em máquinas com um .gitignore global que ignora .claude/, o repositório precisa de !.claude/ para versionar a configuração do time.
- Instruções não são garantias: o CLAUDE.md é contexto, não regra imposta. Para bloquear algo de fato, use deny ou um hook.
- Skill com efeito colateral é manual: deploy, commit e migração levam disable-model-invocation: true, para só rodarem quando você chamar.
- Descrição de skill específica: "ajuda com endpoints" é vaga; "Adiciona um endpoint à Tarefas API (rota em TarefaEndpoints.cs, teste em tests/)" diz quando usar.
- Um objetivo por conversa: use /clear ao trocar de tarefa e /rewind quando o rumo errou, em vez de empilhar correções.
- Revise o diff, sempre: o agente roda os testes, mas não substitui a leitura. Peça testes de erro, não só de sucesso.
- Não confunda autonomia com confiança: modos mais livres (auto, acceptEdits) combinam com código coberto por testes e em repositório com git limpo.
Quando usar o Claude Code em projetos .NET e quando evitar
Use o Claude Code quando:
- A tarefa envolve vários arquivos e comandos: criar uma feature com testes, migrar um padrão, investigar um erro de build.
- O projeto tem comandos de build e teste confiáveis, que dão o critério de pronto.
- Você repete roteiros (novo endpoint, revisão, release) e pode transformá-los em skills.
Evite (ou reduza a autonomia) quando:
- Não há testes nem build automatizado: o agente não tem como saber se acertou, e você revisará tudo na mão.
- O trabalho exige decisão de arquitetura que ainda não foi tomada: converse e decida antes, depois peça a execução.
- Há dados sensíveis, produção ou credenciais ao alcance: isole o ambiente e use regras
deny. - É uma edição de uma linha: o editor resolve mais rápido.
Se a necessidade for dar acesso do modelo a sistemas externos (banco, API interna), o caminho é o MCP do artigo anterior: um servidor MCP é registrado no Claude Code com claude mcp add.
claude mcp add --transport stdio pedidos -- \
dotnet run --project src/PedidosMcpServer --no-buildConclusão: contexto, limites, ritmo e skills no Claude Code
A jornada tem uma ordem. Primeiro o contexto (CLAUDE.md), depois os limites (settings.json e modos de permissão), depois o ritmo (pedir com objetivo e critério de pronto, planejar, iterar em passos curtos, confirmar o que importa) e, por fim, os atalhos (skills). Cada passo deixou um arquivo no repositório, e o projeto de exemplo saiu compilando e com os cinco testes passando.
A reflexão prática: o que separa uma boa sessão de uma ruim está menos no modelo e mais no que você escreveu antes de pedir. Trate o CLAUDE.md, as regras e as skills como código do projeto: versione, revise em pull request e melhore a cada vez que o Claude errar por falta de contexto.
Referências da Parte 2: docs do Claude Code e repositório
- Código do artigo no GitHub: ceseidl/claude-code-dotnet
- Claude Code: Quickstart
- Claude Code: Como o Claude lembra do seu projeto (CLAUDE.md)
- Claude Code: Skills
- Claude Code: Settings e Permissões
- Claude Code: Modos de permissão
- Claude Code: Referência de comandos e MCP
- Série: MCP em .NET: Conectando Modelos de IA a Ferramentas e Dados (ceseidl/mcp-dotnet)
Leia também
- Claude Code com .NET, Parte 1: reúne a base da série: configuração, permissões, ritmo de trabalho e skills.
- Estrutura de um Projeto Claude Code: mostra onde cada arquivo de configuração usado no projeto deve ficar.
- Minimal APIs em ASP.NET Core: aprofunda as Minimal APIs, o tipo de API usado no projeto de exemplo.
Quer levar IA e Claude Code para o fluxo do seu time .NET? Conheça o serviço de backend e arquitetura .NET.
