DEV Community

CPFHub
CPFHub

Posted on

CPF em C# e .NET: validação, FluentValidation e consulta com HttpClient tipado

Transparência: sou fundador da CPFHub, citada no final como uma das opções. A validação mostrada aqui não depende de nenhum fornecedor.

Em projetos .NET brasileiros, CPF aparece em cadastro, emissão de nota, integração com ERP e onboarding. Este guia cobre: uma função de validação sem alocações desnecessárias, a regra no FluentValidation e uma consulta HTTP com IHttpClientFactory e Polly.

1. Validação com ReadOnlySpan<char>

public static class Cpf
{
    public static string Normalizar(string? s) =>
        new string((s ?? "").Where(char.IsAsciiDigit).ToArray());

    public static bool Valido(string? entrada)
    {
        var cpf = Normalizar(entrada).AsSpan();
        if (cpf.Length != 11) return false;

        bool todosIguais = true;
        for (int i = 1; i < 11; i++) if (cpf[i] != cpf[0]) { todosIguais = false; break; }
        if (todosIguais) return false;

        for (int t = 9; t < 11; t++)
        {
            int soma = 0;
            for (int i = 0; i < t; i++) soma += (cpf[i] - '0') * (t + 1 - i);
            int dv = soma * 10 % 11 % 10;
            if (cpf[t] - '0' != dv) return false;
        }
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Use char.IsAsciiDigit (NET 7+) em vez de char.IsDigit: o segundo aceita dígitos de outros alfabetos, como os arábico-índicos, que não fazem sentido aqui.

2. FluentValidation

public class CadastroValidator : AbstractValidator<CadastroDto>
{
    public CadastroValidator()
    {
        RuleFor(x => x.Nome).NotEmpty().MaximumLength(120);
        RuleFor(x => x.Cpf)
            .NotEmpty()
            .Must(Cpf.Valido).WithMessage("CPF inválido");
    }
}
Enter fullscreen mode Exit fullscreen mode

Se você usa Data Annotations, crie um ValidationAttribute chamando o mesmo Cpf.Valido. O importante é ter uma implementação, testada, usada em todo lugar.

3. Persistência

No EF Core, guarde só dígitos e garanta unicidade:

modelBuilder.Entity<Cliente>()
    .Property(c => c.Cpf).HasMaxLength(11).IsFixedLength();
modelBuilder.Entity<Cliente>()
    .HasIndex(c => c.Cpf).IsUnique();
Enter fullscreen mode Exit fullscreen mode

char(11) comunica a intenção e evita que alguém grave com máscara.

4. HttpClient tipado

O DV garante formato. Para saber se o CPF existe e obter nome e data de nascimento, você precisa de uma API. Registre um client tipado:

builder.Services.AddHttpClient<CpfClient>(c =>
{
    c.BaseAddress = new Uri("https://api.cpfhub.io/");
    c.Timeout = TimeSpan.FromSeconds(10);
    c.DefaultRequestHeaders.Add("x-api-key", builder.Configuration["CpfHub:ApiKey"]);
});
Enter fullscreen mode Exit fullscreen mode
public record CpfData(string Cpf, string Name, string Gender, string BirthDate);
public record CpfResponse(bool Success, CpfData? Data);

public class CpfClient(HttpClient http)
{
    public async Task<CpfData?> ConsultarAsync(string cpf, CancellationToken ct = default)
    {
        var limpo = Cpf.Normalizar(cpf);
        if (!Cpf.Valido(limpo)) throw new ArgumentException("CPF inválido");

        using var res = await http.GetAsync($"cpf/{limpo}", ct);
        switch ((int)res.StatusCode)
        {
            case 200:
                var body = await res.Content.ReadFromJsonAsync<CpfResponse>(
                    new JsonSerializerOptions(JsonSerializerDefaults.Web), ct);
                return body?.Data;
            case 404:
                return null; // não encontrado
            case 429:
                var retry = res.Headers.RetryAfter?.Delta;
                throw new LimiteException(retry);
            default:
                res.EnsureSuccessStatusCode();
                return null;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

JsonSerializerDefaults.Web faz o binding case-insensitive (name → Name).

5. Polly: retry só no que faz sentido

Com Microsoft.Extensions.Http.Resilience (.NET 8):

builder.Services.AddHttpClient<CpfClient>(...)
    .AddStandardResilienceHandler(o =>
    {
        o.Retry.MaxRetryAttempts = 2;
        o.Retry.ShouldHandle = args => ValueTask.FromResult(
            args.Outcome.Result?.StatusCode is HttpStatusCode.BadGateway
                or HttpStatusCode.ServiceUnavailable);
    });
Enter fullscreen mode Exit fullscreen mode

Não repita 401, 403 e 404: o resultado não muda. Para 429, respeite o Retry-After em vez de tentar de novo na hora.

6. Não guarde mais do que precisa

A LGPD pede minimização. Se você só precisa confirmar que o nome bate, guarde o resultado da verificação (ok/divergente e data), não necessariamente todos os campos retornados.

7. Testes

[Theory]
[InlineData("529.982.247-25", true)]
[InlineData("11111111111", false)]
[InlineData("52998224724", false)]
public void Valida(string cpf, bool esperado) => Assert.Equal(esperado, Cpf.Valido(cpf));
Enter fullscreen mode Exit fullscreen mode

Para o CpfClient, injete um HttpMessageHandler falso que devolve 200, 404 e 429.

Sobre a CPFHub

A CPFHub é uma das APIs que você pode plugar no CpfClient: GET /cpf/{cpf} com x-api-key, ~150 ms de tempo típico (não SLA), 404 sem consumo de crédito e 50 créditos grátis por mês. Veja a documentação.

Top comments (0)