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; } } 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"); } } 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(); 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"]); }); 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; } } } 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); }); 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)); 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.


