API de pedidos desenvolvida com .NET 10 Minimal API para demonstrar na prática os conceitos de Trunk-Based Development (TBD), Feature Flags e Rollout Gradual usando o pacote Microsoft.FeatureManagement.
- Visão geral
- Pré-requisitos
- Instalação e execução
- Estrutura do projeto
- Endpoints
- Feature Flags
- Rollout gradual
- Configuração por ambiente
- Adaptando para JWT
- Conceitos aplicados
- CHANGELOG
O projeto simula uma API de pedidos com uma feature de desconto progressivo protegida por feature flag. A feature pode ser:
- Desligada — comportamento padrão, sem desconto (seguro para produção)
- Ligada para todos — todos os usuários recebem desconto
- Liberada gradualmente — apenas uma porcentagem dos usuários recebe desconto, controlada por
userIdde forma determinística
Esse padrão permite que o time faça commits na branch main com código incompleto ou experimental sem impacto em produção — a flag protege o usuário final.
| Ferramenta | Versão mínima | Link |
|---|---|---|
| .NET SDK | 10.0 | dotnet.microsoft.com |
| VS Code | Qualquer | code.visualstudio.com |
| C# Dev Kit | Qualquer | Extensão do VS Code |
# 1. Clonar ou extrair o projeto
cd PedidosApi
# 2. Restaurar dependências
dotnet restore
# 3. Rodar a API
dotnet runA API sobe em http://localhost:5000/swagger.
PedidosApi/
├── Program.cs # Registro de serviços, filtros e pipeline HTTP
├── PedidosApi.csproj # Definição do projeto e dependências NuGet
├── appsettings.json # Configuração de produção — flags desligadas por padrão
├── appsettings.Development.json # Configuração de desenvolvimento — flags ligadas para devs
├── CHANGELOG.md # Histórico de versões do projeto
├── Features/
│ ├── FeatureFlags.cs # Constantes dos nomes das flags
│ ├── DescontoService.cs # Lógica de negócio protegida pela feature flag
│ └── UserIdPercentageFilter.cs # Filtro customizado de rollout gradual por userId
└── Endpoints/
└── PedidoEndpoints.cs # Definição dos endpoints da Minimal API
FeatureFlags.cs — centraliza os nomes das flags como constantes tipadas. Evita typos e garante que o compilador aponte todos os usos caso o nome mude.
DescontoService.cs — contém a lógica de negócio do desconto. Consulta o IFeatureManager para decidir qual caminho executar. Não sabe nada sobre porcentagem ou rollout — essa responsabilidade é do filtro.
UserIdPercentageFilter.cs — implementa o rollout gradual determinístico. Calcula um bucket de 0 a 99 a partir do hash do userId e compara com a porcentagem configurada no appsettings.json. O mesmo userId sempre cai no mesmo bucket.
PedidoEndpoints.cs — mapeia os três endpoints da API usando o padrão Minimal API do .NET 8.
Retorna a lista de pedidos de exemplo.
curl http://localhost:5000/pedidosResposta:
[
{ "id": 1, "valor": 100.0, "clientePremium": false },
{ "id": 2, "valor": 250.0, "clientePremium": true },
{ "id": 3, "valor": 80.0, "clientePremium": false }
]Calcula o desconto de um pedido. O comportamento varia conforme o estado da feature flag e o userId informado no header.
Headers:
| Header | Obrigatório | Descrição |
|---|---|---|
Content-Type |
Sim | application/json |
X-User-Id |
Não | Identificador do usuário para o rollout gradual. Se ausente, usa anonymous. |
Body:
{
"valor": 200.00,
"clientePremium": true
}Exemplo — cliente premium:
curl -X POST http://localhost:5000/pedidos/calcular-desconto \
-H "Content-Type: application/json" \
-H "X-User-Id: user-042" \
-d '{"valor": 200, "clientePremium": true}'Resposta com flag desligada:
{
"valor": 200.00,
"clientePremium": true,
"desconto": 0.00,
"total": 200.00,
"featureAtiva": false
}Resposta com flag ligada — cliente premium (15% de desconto):
{
"valor": 200.00,
"clientePremium": true,
"desconto": 30.00,
"total": 170.00,
"featureAtiva": true
}Resposta com flag ligada — cliente padrão (5% de desconto):
{
"valor": 200.00,
"clientePremium": false,
"desconto": 10.00,
"total": 190.00,
"featureAtiva": true
}Regras de desconto:
| Tipo de cliente | Flag ativa | Desconto |
|---|---|---|
| Premium | Sim | 15% sobre o valor |
| Padrão | Sim | 5% sobre o valor |
| Qualquer | Não | 0% — comportamento atual |
Retorna o status atual de todas as feature flags. Útil para debug e validação durante o rollout.
curl http://localhost:5000/feature-flagsResposta:
{
"descontoPremium": false,
"novoCalculoFrete": false
}As flags são configuradas na seção FeatureManagement do appsettings.json. O projeto usa o pacote Microsoft.FeatureManagement.AspNetCore.
| Flag | Descrição | Status padrão |
|---|---|---|
DescontoPremium |
Habilita o cálculo de desconto progressivo por tipo de cliente | false |
NovoCalculoFrete |
Reservada para futura feature de cálculo de frete | false |
"FeatureManagement": {
"DescontoPremium": true,
"NovoCalculoFrete": false
}Avalia por requisição — não determinístico. A mesma chamada pode retornar resultados diferentes para o mesmo usuário.
"FeatureManagement": {
"DescontoPremium": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": { "Value": 20 }
}
]
}
}
⚠️ Use oPercentageFilternativo apenas para testes de carga ou smoke tests. Para rollout de produto com experiência consistente por usuário, use oUserIdPercentageFilterdescrito abaixo.
O UserIdPercentageFilter é um filtro customizado que garante que o mesmo usuário sempre veja (ou não veja) a feature, independente de quantas requisições ele fizer.
userId "user-042"
→ hash: Math.Abs("user-042".GetHashCode())
→ bucket: hash % 100 → resultado: 7
→ Percentage configurado: 20
→ 7 < 20 → true → usuário VÊ a feature
userId "user-001"
→ bucket: 23
→ 23 < 20 → false → usuário NÃO vê a feature
O bucket de cada userId é fixo — não muda entre requisições nem entre deploys.
"FeatureManagement": {
"DescontoPremium": {
"EnabledFor": [
{
"Name": "UserIdPercentage",
"Parameters": { "Percentage": 20 }
}
]
}
}| Fase | Percentage | Duração | Objetivo | Critério para avançar |
|---|---|---|---|---|
| Validação interna | 5 |
1-2 dias | QA e beta testers | Zero erros 5xx novos |
| Canary release | 20 |
2-3 dias | Primeiros usuários reais | Métricas estáveis |
| Rollout ampliado | 50 |
1-2 dias | Metade da base | Latência p95 estável |
| Completo | 100 |
— | Todos os usuários | Alguns dias estável |
| Cleanup | — | 1 dia | Remover código da flag | Feature 100% OK |
# user-042 — bucket 7 — VÊ a feature com Percentage: 20
curl -X POST http://localhost:5000/pedidos/calcular-desconto \
-H "Content-Type: application/json" \
-H "X-User-Id: user-042" \
-d '{"valor": 300, "clientePremium": true}'
# user-001 — bucket 23 — NÃO vê a feature com Percentage: 20
curl -X POST http://localhost:5000/pedidos/calcular-desconto \
-H "Content-Type: application/json" \
-H "X-User-Id: user-001" \
-d '{"valor": 300, "clientePremium": true}'
# Repetir com user-042 — sempre o mesmo resultado (determinístico)Para reverter instantaneamente sem novo deploy, basta alterar o Percentage para 0 e reiniciar:
"Parameters": { "Percentage": 0 }O .NET carrega automaticamente o arquivo correto com base na variável ASPNETCORE_ENVIRONMENT.
| Arquivo | Ambiente | Comportamento |
|---|---|---|
appsettings.json |
Produção | Todas as flags false por padrão |
appsettings.Development.json |
Development | DescontoPremium: true para facilitar o desenvolvimento |
Para forçar um ambiente manualmente:
# Linux / Mac
ASPNETCORE_ENVIRONMENT=Production dotnet run
# Windows PowerShell
$env:ASPNETCORE_ENVIRONMENT="Production"; dotnet runO UserIdPercentageFilter lê o userId do header X-User-Id por simplicidade. Em produção, substitua pela leitura do claim do token JWT:
// Em Features/UserIdPercentageFilter.cs
// Substituir:
var userId = http.HttpContext?
.Request.Headers["X-User-Id"]
.FirstOrDefault()
?? "anonymous";
// Por:
var userId = http.HttpContext?
.User.FindFirst("sub")?.Value // claim "sub" padrão OAuth2/OIDC
?? "anonymous";| Conceito | Descrição | Onde está no código |
|---|---|---|
| Trunk-Based Development | Integração contínua na branch main sem branches de vida longa |
Estratégia de branching do projeto |
| Feature Flag simples | Liga/desliga uma funcionalidade sem novo deploy | appsettings.json + DescontoService.cs |
| Deploy ≠ Release | O código existe em produção mas está inativo enquanto a flag está false |
DescontoService.cs — caminho return 0 |
| Rollout gradual | Liberar para porcentagem crescente de usuários de forma controlada | UserIdPercentageFilter.cs |
| Determinismo por userId | Mesmo usuário sempre cai no mesmo grupo — experiência consistente | Hash % 100 em UserIdPercentageFilter.cs |
| Rollback instantâneo | Reverter a feature sem reverter commit ou fazer deploy | Alterar Percentage: 0 no appsettings.json |
Consulte o arquivo CHANGELOG.md para o histórico completo de versões.