Biblioteca .NET para validar números de identificación españoles: NIF, DNI, NIE y CIF.
Proporciona métodos síncronos, asíncronos, individuales y por lotes para verificar el formato y los dígitos de control de documentos de identificación fiscal y personal de España.
Incluye dos motores:
- API clásica (
ValidadorDocumentos): completa, con objetos de valor, excepciones, mensajes detallados. - API UltraFast (
ValidadorDocumentosUltraFast): SIMD + zero-alloc + particionado por CPU, hasta ~50× más rápida sobre millones de documentos.
- Validación completa de DNI, NIE, NIF (incluye K/L/M) y CIF
- Una línea para empezar:
DocumentoEspanol.EsValido("12345678Z"), sin instanciar nada - Dos motores, un mismo veredicto:
ValidadorDocumentos— API completa: mensajes detallados, objetos de valor, excepcionesValidadorDocumentosUltraFast— motor SIMD zero-alloc para volumen masivo
- Cuatro estilos de API, según lo que necesites:
- Fachada estática (
DocumentoEspanol.EsValido) — lo más directo - Métodos booleanos (
EsDniValido) — cero asignaciones, no lanzan - Métodos con resultado (
IntentarValidarDni) — todos los errores detallados - Métodos con excepción (
ValidarDni) — para flujos "esto no debería fallar"
- Fachada estática (
- Registro en DI de una línea:
services.AddValidacionDocumentosEspanoles() - Multi-target
net8.0/net9.0/net10.0, AOT-friendly y trimmable - API
ReadOnlySpan<char>para parsers y pipelines sin asignaciones - Validación por lotes (síncrona y asíncrona) con paralelismo y orden de entrada garantizado
- Streaming perezoso (
IEnumerable/IAsyncEnumerable/Channel<T>) con memoria constante - Cancelación real: los
*Asynccortan a mitad de proceso, no sólo antes de empezar - Tolerante a
nully a entradas anómalas: nunca lanza por el documento en sí - Serialización binaria extrema con
MemoryPack(Cysharp) - Composición de strings zero-alloc con
ZString(Cysharp) - Objetos de valor (Value Objects) inmutables:
Dni,Nie,Nif,Cif - 100% documentado con XML docs en español; símbolos y SourceLink en el paquete
- Normalización automática (elimina espacios, guiones, puntos)
- Utilidades para calcular letras/dígitos de control
- Resultado de lote completo con válidos, inválidos, estadísticas y porcentajes
- 735 tests, ejecutados en
net8.0ynet10.0
| Target | Soportado |
|---|---|
.NET 8.0 (LTS) |
✅ |
.NET 9.0 |
✅ |
.NET 10.0 |
✅ |
Sin dependencias de plataforma. Compatible con AOT nativo y trimming
(IsAotCompatible / IsTrimmable). El paquete incluye símbolos (.snupkg) y
SourceLink, así que puedes depurar dentro de la librería con F11.
dotnet add package NIF.DNI.NIE.CIF.Validationdotnet nuget add source https://nuget.pkg.github.com/Cristiancastt/index.json --name github --username TU_USUARIO --password TU_GITHUB_TOKEN
dotnet add package NIF.DNI.NIE.CIF.ValidationLo más corto que puede ser: una línea, sin instanciar nada.
using NIF.DNI.NIE.CIF.Validation;
bool ok = DocumentoEspanol.EsValido("12345678Z"); // detecta el tipo solo
bool dni = DocumentoEspanol.EsDniValido("12.345.678-z"); // normaliza solo
TipoDocumento tipo = DocumentoEspanol.DetectarTipo("B12345678"); // CIF
// ¿Por qué falla?
var r = DocumentoEspanol.Validar("12345678A");
Console.WriteLine(r.Mensaje); // "La letra de control del DNI es incorrecta."DocumentoEspanol no lanza excepciones, no asigna memoria y acepta null
como entrada inválida. Cuando necesites mensajes detallados, objetos de valor
o inyección de dependencias, usa la API completa:
using NIF.DNI.NIE.CIF.Validation.Implentaciones;
using NIF.DNI.NIE.CIF.Validation.Excepciones;
var validador = new ValidadorDocumentos();
// ✅ Validación booleana simple
bool esValido = validador.EsDniValido("12345678Z");
// ✅ Validación con resultado detallado (sin excepción)
var resultado = validador.IntentarValidarDni("12345678Z");
if (resultado.EsValido)
Console.WriteLine(resultado.Mensaje);
// ✅ Validación con excepción si es inválido
try
{
var documento = validador.ValidarDni("12345678Z");
Console.WriteLine($"Control: {documento.CaracterControl}");
}
catch (DocumentoNoValidoException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}| Tipo | Formato | Ejemplo | Descripción |
|---|---|---|---|
| DNI | 8 dígitos + 1 letra | 12345678Z |
Documento Nacional de Identidad |
| NIE | X/Y/Z + 7 dígitos + 1 letra | X1234567L |
Número de Identidad de Extranjero |
| NIF | DNI, NIE o K/L/M + 7 dígitos + letra | K1234567A |
Número de Identificación Fiscal |
| CIF | Letra + 7 dígitos + control | B12345678 |
Código de Identificación Fiscal |
bool EsDniValido(string dni);
bool EsNieValido(string nie);
bool EsNifValido(string nif);
bool EsCifValido(string cif);
bool EsDocumentoValido(string documento); // Auto-detecta tipoLanzan DocumentoNoValidoException si el documento no es válido. Devuelven DocumentoValidado.
DocumentoValidado ValidarDni(string dni);
DocumentoValidado ValidarNie(string nie);
DocumentoValidado ValidarNif(string nif);
DocumentoValidado ValidarCif(string cif);
DocumentoValidado ValidarDocumento(string documento); // Auto-detecta tipoDevuelven ResultadoValidacion sin lanzar excepciones.
ResultadoValidacion IntentarValidarDni(string dni);
ResultadoValidacion IntentarValidarNie(string nie);
ResultadoValidacion IntentarValidarNif(string nif);
ResultadoValidacion IntentarValidarCif(string cif);
ResultadoValidacion IntentarValidarDocumento(string documento);ResultadoValidacionLote ValidarLoteDni(IEnumerable<string> dnis);
ResultadoValidacionLote ValidarLoteNie(IEnumerable<string> nies);
ResultadoValidacionLote ValidarLoteNif(IEnumerable<string> nifs);
ResultadoValidacionLote ValidarLoteCif(IEnumerable<string> cifs);
ResultadoValidacionLote ValidarLoteDocumentos(IEnumerable<string> documentos);Task<ResultadoValidacionLote> ValidarLoteDniAsync(IEnumerable<string> dnis, CancellationToken ct = default);
Task<ResultadoValidacionLote> ValidarLoteNieAsync(IEnumerable<string> nies, CancellationToken ct = default);
Task<ResultadoValidacionLote> ValidarLoteNifAsync(IEnumerable<string> nifs, CancellationToken ct = default);
Task<ResultadoValidacionLote> ValidarLoteCifAsync(IEnumerable<string> cifs, CancellationToken ct = default);
Task<ResultadoValidacionLote> ValidarLoteDocumentosAsync(IEnumerable<string> documentos, CancellationToken ct = default);TipoDocumento DetectarTipo(string? documento);
string Normalizar(string? documento);
char ObtenerLetraDni(int numero);
char ObtenerLetraNie(string nie);
(char Digito, char Letra) ObtenerControlCif(string cif);Todos los métodos de validación aceptan
string?:nulles una entrada inválida, nunca unaArgumentNullException.
Los Value Objects son inmutables y solo pueden crearse con valores válidos:
using NIF.DNI.NIE.CIF.Validation.Modelos;
// Crear un DNI válido (lanza excepción si inválido)
var dni = Dni.Crear("12345678Z");
Console.WriteLine(dni.Numero); // 12345678
Console.WriteLine(dni.Letra); // 'Z'
// Intentar crear sin excepción (patrón TryParse)
if (Dni.Intentar("12345678Z", out var dniValido))
Console.WriteLine(dniValido!.Valor);
// Generar un DNI a partir de un número
var dniGenerado = Dni.DesdeNumero(12345678);
// Crear un NIE
var nie = Nie.Crear("X1234567L");
Console.WriteLine(nie.LetraInicial); // 'X'
// Crear un CIF
var cif = Cif.Crear("B12345678");
Console.WriteLine(cif.TipoEntidad); // "Sociedad de Responsabilidad Limitada"
Console.WriteLine(cif.CodigoProvincia); // "12"
// Conversión implícita a string
string texto = dni; // "12345678Z"var validador = new ValidadorDocumentos();
var documentos = new[]
{
"12345678Z", // DNI
"X1234567L", // NIE
"B12345678", // CIF
"INVALIDO", // Inválido
"00000000T", // DNI
"999", // Inválido
};
// Síncrono
var lote = validador.ValidarLoteDocumentos(documentos);
Console.WriteLine(lote.Resumen);
// "Procesados: 6 | Válidos: 4 (66.67%) | Inválidos: 2 (33.33%)"
Console.WriteLine($"Todos válidos: {lote.TodosValidos}"); // false
Console.WriteLine($"Tiene errores: {lote.TieneInvalidos}"); // true
// Iterar válidos
foreach (var valido in lote.Validos)
Console.WriteLine($" {valido.TipoDocumento}: {valido.ValorNormalizado}");
// Iterar inválidos
foreach (var error in lote.Invalidos)
Console.WriteLine($" Error: {error.Valor} - {error.Mensaje}");
// Filtrar por tipo
var dnis = lote.ObtenerValidosPorTipo(TipoDocumento.DNI);
// Asíncrono con cancelación
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var loteAsync = await validador.ValidarLoteDocumentosAsync(documentos, cts.Token);| Propiedad | Tipo | Descripción |
|---|---|---|
EsValido |
bool |
Si el documento es válido |
ValorOriginal |
string |
Valor tal como fue proporcionado |
ValorNormalizado |
string |
Valor sin espacios/guiones, en mayúsculas |
TipoDocumento |
TipoDocumento |
Tipo detectado |
Mensaje |
string |
Mensaje descriptivo |
CaracterControl |
string? |
Letra o dígito de control |
CaracterControlEsperado |
string? |
Control esperado (para depuración) |
| Propiedad | Tipo | Descripción |
|---|---|---|
Validos |
List<DocumentoValidado> |
Documentos válidos |
Invalidos |
List<ErrorValidacion> |
Errores de validación |
ResultadosIndividuales |
List<ResultadoValidacion> |
Todos los resultados |
TotalProcesados |
int |
Total procesado |
TotalValidos |
int |
Cantidad de válidos |
TotalInvalidos |
int |
Cantidad de inválidos |
TodosValidos |
bool |
Si todos son válidos |
TodosInvalidos |
bool |
Si todos son inválidos |
TieneValidos |
bool |
Si hay algún válido |
TieneInvalidos |
bool |
Si hay algún inválido |
PorcentajeValidos |
double |
% válidos (0-100) |
PorcentajeInvalidos |
double |
% inválidos (0-100) |
Resumen |
string |
Resumen textual |
| Propiedad | Tipo | Descripción |
|---|---|---|
ValorOriginal |
string |
Valor original |
ValorNormalizado |
string |
Valor normalizado |
TipoDocumento |
TipoDocumento |
Tipo de documento |
CaracterControl |
string? |
Carácter de control |
ParteNumerica |
string |
Parte numérica |
| Propiedad | Tipo | Descripción |
|---|---|---|
Valor |
string |
Valor original |
ValorNormalizado |
string |
Valor normalizado |
Mensaje |
string |
Descripción del error |
TipoDocumentoEsperado |
TipoDocumento |
Tipo esperado |
CodigoError |
string |
Código programático |
using NIF.DNI.NIE.CIF.Validation.Extensiones;
// En Program.cs: registra IValidadorDocumentos, ValidadorDocumentosUltraFast
// y ProcesadorLotesUltraFast, todos como singleton (no tienen estado mutable).
builder.Services.AddValidacionDocumentosEspanoles();
// Opcional: limitar el paralelismo de los lotes (p.ej. en un contenedor con cuota)
builder.Services.AddValidacionDocumentosEspanoles(gradoParalelismoLotes: 2);
// En tu servicio/controlador
public class MiServicio
{
private readonly IValidadorDocumentos _validador;
public MiServicio(IValidadorDocumentos validador)
{
_validador = validador;
}
public bool VerificarCliente(string documentoIdentidad)
{
return _validador.EsDocumentoValido(documentoIdentidad);
}
}Pensada para procesar millones de DNIs/NIEs/CIFs en streaming o batch. No reemplaza la API clásica: ambas coexisten y son 100% compatibles en resultados.
using NIF.DNI.NIE.CIF.Validation.UltraFast;
var ultra = ValidadorDocumentosUltraFast.Instancia;
// Atajo booleano (normaliza internamente)
bool esValido = ultra.EsDniValido("12345678Z");
// Resultado completo: tipo, carácter de control, código y mensaje de error
ResultadoValidacionUltra r = ultra.ValidarDni("12345678Z");
Console.WriteLine($"{r.EsValido} {r.Tipo} {r.CaracterControl}");
// Sobre Span<char>: cero asignaciones, para parsers y pipelines.
// OJO: el span debe venir YA normalizado (mayúsculas, sin puntos ni guiones).
ResultadoValidacionUltra s = ultra.ValidarDniSpan("12345678Z".AsSpan());
// Auto-detección de tipo sobre Span
var t = ultra.ValidarDocumentoSpan("X1234567L".AsSpan()); // TipoDocumentoUltra.NIEvar proc = new ProcesadorLotesUltraFast(); // 1 partición por CPU
// 1 millón de DNIs en ~15 ms. resultados[i] corresponde siempre a dnis[i].
ResultadoValidacionUltra[] resultados = proc.ValidarLote(dnis);
// Versión async: descarga el trabajo del hilo llamante.
// El token se comprueba DURANTE el proceso, no sólo al planificarlo: cancelar
// a mitad de un lote de 50 millones lo detiene de verdad.
var resultadosAsync = await proc.ValidarLoteAsync(dnis, ct);
// Versión compatible con la API clásica (devuelve ResultadoValidacionLote)
var lote = proc.ValidarLoteCompat(dnis, ct);
var loteAsync = await proc.ValidarLoteCompatAsync(dnis, ct);Los métodos ValidarLote* materializan el origen entero en un array. Para un
fichero de millones de líneas eso es un OutOfMemoryException. Usa el stream:
// Memoria constante, sea cual sea el tamaño del fichero
foreach (var r in proc.ValidarStream(File.ReadLines("padron.csv"), ct))
if (!r.EsValido) errores++;
// Origen asíncrono
await foreach (var r in proc.ValidarStreamAsync(File.ReadLinesAsync("padron.csv"), ct))
if (!r.EsValido) errores++;
// La API clásica también lo ofrece, con mensajes de error detallados
foreach (var r in validador.ValidarStream(File.ReadLines("padron.csv"), ct))
if (!r.EsValido) log.Warn(r.Mensaje);Para pipelines con productor y consumidor desacoplados:
var entrada = Channel.CreateUnbounded<string?>();
var salida = Channel.CreateBounded<ResultadoValidacionUltra>(1024); // backpressure
var productor = Task.Run(async () =>
{
foreach (var dni in colaDeDnis)
await entrada.Writer.WriteAsync(dni, ct);
entrada.Writer.Complete(); // imprescindible: marca el fin de la entrada
});
// Arranca el pipeline y CONSUME EN PARALELO. Si esperas a que termine
// `pipeline` antes de leer y el canal de salida es acotado, se llena y los
// workers se quedan bloqueados: interbloqueo.
var pipeline = proc.ProcesarStreamAsync(entrada.Reader, salida.Writer, ct);
await foreach (var r in salida.Reader.ReadAllAsync(ct))
Console.WriteLine($"{r.EsValido} {r.Tipo}");
await productor;
await pipeline; // propaga cualquier fallo del pipelineProcesarStreamAsync siempre completa el canal de salida al terminar —
también si un worker falla o si se cancela —, de modo que el await foreach
del consumidor nunca se queda esperando indefinidamente.
Si no necesitas dos canales, ValidarStream / ValidarStreamAsync hacen lo
mismo con mucha menos ceremonia.
byte[] bytes = ExportadorResultados.SerializarLote(lote);
var dto = ExportadorResultados.DeserializarLote(bytes);string resumen = ProcesadorLotesUltraFast.ResumenLoteZString(
total: 10_000_000, validos: 9_800_000, invalidos: 200_000);
// "Procesados: 10000000 | Válidos: 9800000 (98%) | Inválidos: 200000 (2%)"Comparativa entre la API clásica y la API UltraFast. Máquina: 13th Gen Intel Core i5-1335U, 12 lógicos / 10 físicos. Job: ShortRun (3 iteraciones, warmup 3). Mezcla realista: 70 % DNIs, 15 % NIEs, 10 % CIFs, 5 % inválidos.
| Método | Tiempo | Memoria |
|---|---|---|
Clásica.EsDniValido |
~35 ns | 0 B |
Ultra.ValidarDni (string) |
~16 ns | 0 B |
Ultra.ValidarDniSpan (Span) |
~7 ns | 0 B |
La API clásica venía de 379 ns y 856 B por llamada: construía cinco objetos regla, una lista y una consulta LINQ en cada validación, y los documentos inválidos además lanzaban y capturaban una excepción (1.296 B). Hoy las tres vías no asignan nada y un documento inválido cuesta lo mismo que uno válido.
| Método | Tiempo | Memoria |
|---|---|---|
Clásica.DetectarTipo |
~17 ns | 0 B |
Ultra.DetectarPrefijo |
~1 ns | 0 B |
| Tamaño | Clásica | Ultra (struct) | Ultra (compat) |
|---|---|---|---|
| 100 k | 82 ms / 39 MB | 2,3 ms / 1,1 MB | 62 ms / 31 MB |
| 1 M | 772 ms / 388 MB | 15 ms / 11 MB | 570 ms / 311 MB |
| 10 M | 8,9 s / 3,8 GB | 90 ms / 114 MB | 5,1 s / 3,0 GB |
| Tamaño | Clásica | Ultra | Speedup |
|---|---|---|---|
| 100 k | 74 ms / 39 MB | 2,0 ms / 1,1 MB | 36× |
| 1 M | 534 ms / 388 MB | 14 ms / 11 MB | 38× |
| 10 M | 5,4 s / 3,8 GB | 114 ms / 114 MB | 47× |
Cómo elegir. Si validas documentos sueltos, cualquiera de las dos APIs vale: la diferencia son nanosegundos. La API ultra gana cuando procesas millones, porque devuelve
structs en lugar de construir un objeto de resultado, unDocumentoValidadoy un mensaje por documento. Ese coste es justo lo que mideUltra (compat): mismo motor, pero materializando los DTOs de la API clásica.Si el origen no cabe en memoria, ninguna de las dos: usa
ValidarStream, que consume memoria constante.
Para correr los benchmarks en local:
dotnet run --project Benchmarks -c Release -- --filter "*BenchmarksLote*"
# O solo una categoría: --anyCategories Individual Prefijo -j short
# O smoke test rápido: -- --smoke- Formato: 8 dígitos + 1 letra
- Letra =
"TRWAGMYFPDXBNJZSQVHLCKE"[número % 23]
- Formato: X/Y/Z + 7 dígitos + 1 letra
- Reemplazar X→0, Y→1, Z→2
- Calcular letra como DNI
- Formato: K/L/M + 7 dígitos + 1 letra
- Calcular letra como DNI sobre los 7 dígitos
- Formato: Letra tipo + 7 dígitos + control
- Posiciones impares (1ª, 3ª, 5ª, 7ª): multiplicar por 2, sumar dígitos del resultado
- Posiciones pares (2ª, 4ª, 6ª): sumar directamente
- Control = (10 - (suma total % 10)) % 10
- Según tipo: A/B/E/H→dígito, K/P/Q/S→letra, resto→ambos
Cosas con las que puedes contar al integrar la librería:
| Garantía | Detalle |
|---|---|
| Ambos motores coinciden | ValidadorDocumentos y ValidadorDocumentosUltraFast dan siempre el mismo veredicto sobre el mismo documento. |
| Orden estable en lotes | En todos los ValidarLote*, síncronos y asíncronos, ResultadosIndividuales[i] corresponde a la entrada i. |
null nunca lanza |
Cualquier entrada (null, vacía, con basura, enorme) devuelve "inválido"; no hay ArgumentNullException por el documento. |
| Cancelación real | Los *Async comprueban el token durante el proceso. Cancelar un lote de millones lo detiene; no sólo evita que empiece. |
| Sin interbloqueos | ProcesarStreamAsync completa el canal de salida pase lo que pase (fin, fallo o cancelación): el consumidor nunca se queda colgado. |
| Memoria acotada | Ni cachés estáticas que crezcan sin tope ni buffers proporcionales a entradas anómalas. Los lotes son lineales; el streaming, constante. |
| Independiente de la cultura | Resumen y los porcentajes usan InvariantCulture: el mismo texto en cualquier máquina. |
| Thread-safe | Ningún tipo público tiene estado mutable. Regístralos como singleton y compártelos. |
| Sin excepciones en el camino feliz | EsXValido, Intentar y la API ultra no lanzan ni capturan internamente; validar un documento inválido no cuesta más que uno válido. |
MIT
Cristian Arana Castiñeiras