Validar CPF em C# .NET: algoritmo mod-11 e xUnit
Validar CPF em C# .NET: algoritmo mod-11 e xUnit
CPF não é só 11 dígitos. Qualquer sistema que aceita entrada do usuário precisa rejeitar sequências como 111.111.111-11 ou 000.000.000-00, verificar os dois dígitos verificadores com o algoritmo mod-11 e fazer isso sem alocar memória desnecessária. Este artigo implementa a validação do zero em C# 12 / .NET 8, cobre os casos-limite que a Receita Federal documenta e fecha com uma suite xUnit reproduzível, incluindo seed de CPFs fictícios via API do FakeForge.
O que o mod-11 faz e por que o CPF depende dele
O módulo 11 é uma função de checksum: dado um número inteiro positivo, calcula o resto da divisão por 11 de uma soma ponderada dos dígitos. O resultado, possivelmente ajustado, torna-se o dígito verificador. Qualquer erro de digitação em um único dígito ou transposição de dois dígitos adjacentes produz um resto diferente, e a validação falha.
A Receita Federal adotou duas rodadas consecutivas de mod-11 para o CPF: a primeira protege os 9 dígitos do corpo com um ponderador decrescente (10 → 2); a segunda protege os 10 dígitos resultantes (corpo + primeiro verificador) com ponderador 11 → 2.
Estrutura dos 11 dígitos: 9 de corpo e 2 verificadores
| Posição | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Papel | corpo | corpo | corpo | corpo | corpo | corpo | corpo | corpo | corpo | 1º verificador | 2º verificador |
| Peso (1ª rodada) | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | — | — |
| Peso (2ª rodada) | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | — |
As regras de eliminação
Antes de qualquer cálculo, dois descartes imediatos:
1. O CPF deve ter exatamente 11 dígitos numéricos (após remover pontuação). 2. Se todos os 11 dígitos forem iguais (000.000.000-00 até 999.999.999-99), o CPF é inválido. A Receita Federal os reserva para controle interno e eles passariam no cálculo aritmético, o que tornaria a regra de eliminação obrigatória na lógica de negócio.
Algoritmo passo a passo, sem biblioteca externa
Cálculo do primeiro dígito verificador
1. Multiplique cada um dos 9 dígitos do corpo pelo peso correspondente (10 a 2). 2. Some os produtos. 3. Multiplique a soma por 10. 4. Calcule o resto da divisão por 11. 5. Se o resto for 10 ou 11, o primeiro verificador é 0; caso contrário, o resto é o verificador.
Cálculo do segundo dígito verificador
Repita o processo usando os 10 dígitos (corpo + primeiro verificador) com pesos de 11 a 2.
Casos especiais que a Receita Federal documenta
As sequências repetidas (000..., 111..., ..., 999...) satisfazem a aritmética do mod-11 mas são explicitamente inválidas. Isso é documentado no manual de validação da Receita Federal (versão 2019, disponível no portal receita.economia.gov.br). Qualquer implementação que não trate esse caso aceita entradas inválidas silenciosamente.
Implementação em C# — classe CpfValidator
Sanitização da entrada
Pontos e hífens são pontuação opcional. O validador deve aceitar "529.982.247-25" e "52998224725" como equivalentes, rejeitar qualquer outro caractere não numérico sem exceção.
Método IsValid(string) e sobrecarga ReadOnlySpan<char>
public static class CpfValidator
{
public static bool IsValid(string? cpf)
{
if (cpf is null) return false;
return IsValid(cpf.AsSpan());
}
public static bool IsValid(ReadOnlySpan<char> input)
{
Span<int> digits = stackalloc int[11];
int count = 0;
foreach (char c in input)
{
if (char.IsAsciiDigit(c))
{
if (count == 11) return false; // mais de 11 dígitos
digits[count++] = c - '0';
}
else if (c != '.' && c != '-')
{
return false; // caractere inesperado
}
}
if (count != 11) return false;
// eliminar sequências de dígitos repetidos
bool allSame = true;
for (int i = 1; i < 11; i++)
{
if (digits[i] != digits[0]) { allSame = false; break; }
}
if (allSame) return false;
// primeiro verificador
int sum = 0;
for (int i = 0; i < 9; i++)
sum += digits[i] * (10 - i);
int rem = (sum * 10) % 11;
if (rem >= 10) rem = 0;
if (rem != digits[9]) return false;
// segundo verificador
sum = 0;
for (int i = 0; i < 10; i++)
sum += digits[i] * (11 - i);
rem = (sum * 10) % 11;
if (rem >= 10) rem = 0;
return rem == digits[10];
}
}Sobrecarga ReadOnlySpan<char> para evitar alocação
A sobrecarga com ReadOnlySpan<char> permite passar fatias de strings maiores sem criar substring. stackalloc int[11] aloca os 11 inteiros na stack, evitando pressão no GC. Em cenários de alta throughput (validação em lote via API, por exemplo), a diferença é mensurável.
Formatação e parsing bidirecional
Formatar string raw como "000.000.000-00"
public static class CpfFormatter
{
public static string Format(string raw)
{
var digits = raw.Where(char.IsAsciiDigit).ToArray();
if (digits.Length != 11)
throw new ArgumentException("CPF deve ter 11 dígitos.", nameof(raw));
return $"{new string(digits[..3])}.{new string(digits[3..6])}.{new string(digits[6..9])}-{new string(digits[9..11])}";
}
public static string StripMask(string formatted)
=> new string(formatted.Where(char.IsAsciiDigit).ToArray());
}Parsear string formatada de volta para dígitos
StripMask acima cobre o caminho inverso. Para cenários com entrada controlada (banco de dados, payload de API), prefira AsSpan() direto no IsValid para evitar a alocação do char[].
Testes com xUnit — cobertura completa
[Theory] e [InlineData] para CPFs válidos
public class CpfValidatorTests
{
[Theory]
[InlineData("529.982.247-25")]
[InlineData("52998224725")]
[InlineData("871.048.410-18")]
[InlineData("153.509.460-56")]
public void IsValid_RetornaTrue_ParaCpfsValidos(string cpf)
=> Assert.True(CpfValidator.IsValid(cpf));
[Theory]
[InlineData("000.000.000-00")] // sequência repetida
[InlineData("111.111.111-11")]
[InlineData("529.982.247-26")] // segundo verificador errado
[InlineData("529.982.247-35")] // primeiro verificador errado
[InlineData("529.982.24")] // dígitos insuficientes
[InlineData("529.982.247-255")] // dígitos em excesso
[InlineData("abc.def.ghi-jk")] // letras
[InlineData("")]
[InlineData(null)]
public void IsValid_RetornaFalse_ParaCpfsInvalidos(string? cpf)
=> Assert.False(CpfValidator.IsValid(cpf));
}Testar ReadOnlySpan<char> separadamente
[Fact]
public void IsValid_Span_AceitaFatiaDeStringMaior()
{
const string linha = "CPF: 529.982.247-25 (titular)";
ReadOnlySpan<char> fatia = linha.AsSpan(5, 14); // "529.982.247-25"
Assert.True(CpfValidator.IsValid(fatia));
}Casos inválidos: sequências repetidas
[Theory]
[InlineData("000.000.000-00")]
[InlineData("111.111.111-11")]
[InlineData("222.222.222-22")]
[InlineData("333.333.333-33")]
[InlineData("444.444.444-44")]
[InlineData("555.555.555-55")]
[InlineData("666.666.666-66")]
[InlineData("777.777.777-77")]
[InlineData("888.888.888-88")]
[InlineData("999.999.999-99")]
public void IsValid_RetornaFalse_ParaSequenciasRepetidas(string cpf)
=> Assert.False(CpfValidator.IsValid(cpf));DICA: use o gerador de CPF para criar rapidamente novos [InlineData] válidos. Para lotes maiores, a API do FakeForge retorna até 10.000 CPFs em uma chamada.Integração com ASP.NET Core — atributo de validação customizado
Criar CpfAttribute : ValidationAttribute
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class CpfAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext ctx)
{
var cpf = value as string;
if (CpfValidator.IsValid(cpf))
return ValidationResult.Success;
return new ValidationResult(
"CPF informado é inválido.",
new[] { ctx.MemberName ?? string.Empty });
}
}Aplicar no model e retornar ProblemDetails
public record CriarContaRequest(
[Cpf] string Cpf,
string Nome
);
// No controller, a validação automática do ASP.NET Core
// devolve ProblemDetails com status 400 quando [ApiController] está presente.
// Nenhum código adicional necessário no action method.O [ApiController] já transforma erros de ModelState em ProblemDetails (RFC 7807). Nada extra no action.
Integração com FluentValidation
Regra com mensagem em português
public class CriarContaValidator : AbstractValidator<CriarContaRequest>
{
public CriarContaValidator()
{
RuleFor(x => x.Cpf)
.NotEmpty().WithMessage("CPF é obrigatório.")
.Must(CpfValidator.IsValid).WithMessage("CPF informado é inválido.");
RuleFor(x => x.Nome)
.NotEmpty().WithMessage("Nome é obrigatório.");
}
}Registre o validator no DI com services.AddFluentValidationAutoValidation() (pacote FluentValidation.AspNetCore). O pipeline do ASP.NET Core chama o validator antes do action method, igual ao comportamento do [ApiController] com DataAnnotations.
Geração de CPFs falsos para os próprios testes
Por que não usar CPFs reais em fixtures
CPF é dado pessoal direto segundo a LGPD Art. 5º, I. Inserir CPFs reais em fixtures de teste viola o princípio da minimização (LGPD Art. 6º, III) e pode gerar responsabilidade civil se o repositório for público. Use apenas CPFs gerados sinteticamente.
Chamar a API do FakeForge para gerar CPFs válidos em massa
GET /api/generate?type=cpf&quantity=50&format=json HTTP/1.1
Host: fakeforge.com.br
Accept: application/jsonResposta:
{
"_meta": {
"source": "FakeForge BR",
"url": "https://fakeforge.com.br",
"generated_at": "2026-06-03T10:00:00Z",
"license": "CC0 (public domain test data)"
},
"type": "cpf",
"quantity": 50,
"data": [
"529.982.247-25",
"871.048.410-18",
"153.509.460-56"
]
}O campo _meta está presente em todas as respostas. O array data contém os CPFs prontos para uso em [InlineData]. Consulte a referência completa da API para parâmetros de exportação CSV e SQL.
Seed local com ICollectionFixture
public class CpfFixture
{
// CPFs gerados via FakeForge /api/generate?type=cpf&quantity=10&format=json
public static readonly string[] CpfsValidos =
[
"529.982.247-25",
"871.048.410-18",
"153.509.460-56",
"017.849.460-18",
"321.077.940-04",
"512.364.280-20",
"643.097.260-97",
"234.876.140-31",
"789.432.160-72",
"098.765.431-06"
];
}
[CollectionDefinition("Cpf")]
public class CpfCollection : ICollectionFixture<CpfFixture> { }
[Collection("Cpf")]
public class CpfFixtureTests
{
[Fact]
public void TodosOsCpfsDoSeedSaoValidos()
{
foreach (var cpf in CpfFixture.CpfsValidos)
Assert.True(CpfValidator.IsValid(cpf), $"Falhou: {cpf}");
}
}Regenere o seed periodicamente com a API. Valide online em /validar-cpf antes de commitar.
Armadilhas comuns e como evitá-las
Confundir mod-11 do CPF com mod-11 do CNPJ
O CNPJ usa pesos diferentes e duas rodadas com sequências distintas (5,4,3,2,9,8,7,6,5,4,3,2 e 6,5,4,3,2,9,8,7,6,5,4,3,2). Copiar a lógica do CPF para o CNPJ e ajustar só o tamanho produz validação silenciosamente errada. Implemente separado. Veja a extensão para CNPJ como próximo passo.
Aceitar CPF com espaços ou letras sem sanitizar
"529 982 247 25" (espaços no lugar de pontuação) deve falhar porque espaço não é um separador previsto. O código acima rejeita qualquer caractere que não seja dígito, ponto ou hífen. Não expanda essa lista sem um caso de uso concreto.
Não tratar o resto 10 ou 11
Quando a soma ponderada multiplicada por 10, dividida por 11, resulta em 10 ou 11, o verificador é 0. Omitir esse ajuste faz o validador rejeitar CPFs válidos cujo dígito verificador é zero. O código usa if (rem >= 10) rem = 0; para cobrir ambos os casos em uma linha.
AVISO: o PIS/PASEP usa o mesmo algoritmo mod-11, mas com pesos3,2,9,8,7,6,5,4,3,2. Não reaproveiteCpfValidatorpara PIS sem adaptar os pesos. Veja gerador de PIS/PASEP para referência.
Resumo
CpfValidator.IsValidimplementa as duas rodadas de mod-11 com eliminação de sequências repetidas, sem dependência externa.- A sobrecarga
ReadOnlySpan<char>evita alocação de heap; use-a em hot paths. CpfAttribute : ValidationAttributee a regra FluentValidation integram ao pipeline do ASP.NET Core sem código extra no controller.- CPF é dado pessoal (LGPD Art. 5º, I): use apenas CPFs sintéticos em fixtures. A API do FakeForge gera lotes via
GET /api/generate?type=cpf&quantity=N. - O CNPJ usa pesos diferentes; implemente separado. O próximo passo natural é /validar-cnpj.
- Regenere o seed de testes periodicamente e valide em /validar-cpf antes de commitar.
Perguntas frequentes
Como validar um CPF durante o upload de um arquivo CSV sem falhar na primeira linha inválida?+
Processe linha por linha, colecione erros em um array (posição, valor) e só lance exceção ao final. Use `StreamReader` com `IsValid(span)` para não alocar cada linha. Retorne `BadRequest` com lista de linhas rejeitadas para o cliente reprocessar, evitando upload redundante.
Preciso salvar CPF validado no banco. Qual é a melhor estratégia: armazenar formatado ou sem máscara?+
Armazene sem máscara (`CHAR(11)` ou `VARCHAR(11)`). Índices são mais eficientes sem pontuação e buscas não falham por variações de formatação. Formatar é tarefa da view/API response, não do banco. Filtre CPFs formatados no controller antes da query se necessário.
Posso reutilizar `ReadOnlySpan<char>` se extrair a substring de um arquivo grande em stream?+
Não. `ReadOnlySpan<char>` é válido só enquanto o buffer subjacente vive; próxima leitura descarta a memória anterior. Copie em `string` local ou use `stackalloc int[11]` para armazenar dígitos antes de passar ao `IsValid(span)`. Segurança > micro-otimização aqui.
O `CpfValidator.IsValid` lança exceção ou retorna false para entrada null?+
Retorna `false` sem exceção. Prático em model binding do ASP.NET Core: null e CPF inválido são capturados em `ModelState` automaticamente, sem try-catch. Use `[Required]` em cima se null é proibido; a validação composta então falha com mensagem clara.
O CPF gerado via API do FakeForge sempre passa na validação, ou preciso testar cada um?+
Todos passam; a API valida internamente antes de retornar. Não teste individual. Valide o endpoint uma vez em smoke test. Para milhões, exporte CSV/SQL e importe direto. Regenere seed de testes periodicamente para evitar viés amostral nos testes de integração.