← Voltar ao blog

Validar CPF em C# .NET: algoritmo mod-11 e xUnit

·12 min de leitura

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ção1234567891011
Papelcorpocorpocorpocorpocorpocorpocorpocorpocorpo1º verificador2º verificador
Peso (1ª rodada)1098765432
Peso (2ª rodada)111098765432

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/json

Resposta:

{
  "_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 pesos 3,2,9,8,7,6,5,4,3,2. Não reaproveite CpfValidator para PIS sem adaptar os pesos. Veja gerador de PIS/PASEP para referência.

Resumo

  • CpfValidator.IsValid implementa 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 : ValidationAttribute e 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.