DgiiEcf 1.0.0
dotnet add package DgiiEcf --version 1.0.0
NuGet\Install-Package DgiiEcf -Version 1.0.0
<PackageReference Include="DgiiEcf" Version="1.0.0" />
<PackageVersion Include="DgiiEcf" Version="1.0.0" />
<PackageReference Include="DgiiEcf" />
paket add DgiiEcf --version 1.0.0
#r "nuget: DgiiEcf, 1.0.0"
#:package DgiiEcf@1.0.0
#addin nuget:?package=DgiiEcf&version=1.0.0
#tool nuget:?package=DgiiEcf&version=1.0.0
DgiiEcf
Facturación electrónica (e-CF) de la DGII de República Dominicana para .NET 8 y .NET 10.
Es un port del paquete Node dgii-ecf (v1.8.5), organizado con Clean Architecture y el patrón Result<T>.
| Área | Qué incluye |
|---|---|
| Firma | XML-DSig con el perfil de la DGII: C14N inclusivo, RSA-SHA256, Reference URI="", enveloped |
| Emisor | Autenticación por semilla con token renovado automáticamente, envío de e-CF, RFCE, aprobación comercial (ACECF), anulación de secuencias (ANECF) y consultas |
| Receptor | Semilla, validación de la semilla firmada con emisión de JWT, validación del token, lectura del multipart y acuse de recibo (ARECF) |
| Herramientas | Código de seguridad, conversión e-CF 32 → RFCE, URL del QR, validación de firmas, JSON ⇄ XML compatible con el paquete Node |
| Documentos tipados | Ecf, Rfce, Arecf, Acecf y Anecf, con el orden de elementos del XSD |
Instalación
dotnet add package DgiiEcf
Configuración
appsettings.json:
{
"DgiiEcf": {
"Environment": "Test",
"StatusApiKey": null,
"Certificate": {
"CertificatePath": "certs/empresa.p12",
"CertificatePassword": "<desde un secret store>"
}
}
}
Los valores posibles de Environment son:
| Valor | Ambiente DGII |
|---|---|
Test |
TesteCF |
Certification |
CerteCF |
Production |
eCF |
El certificado también se puede entregar como base64 en CertificateBase64, algo útil en contenedores y secret managers. Nunca guardes la contraseña en el repositorio.
builder.Services.AddDgiiEcf(builder.Configuration);
La configuración también se puede hacer por código, o partiendo de un X509Certificate2 que ya tengas abierto:
services.AddDgiiEcf(options =>
{
options.Environment = DgiiEnvironment.Certification;
options.CertificateBase64 = secrets["dgii-p12"];
options.CertificatePassword = secrets["dgii-p12-password"];
});
services.AddDgiiEcf(certificate, options => options.Environment = DgiiEnvironment.Production);
Si no usas un contenedor de DI:
using var dgii = DgiiEcfClient.Create(options => { /* ... */ });
var client = dgii.Client; // IDgiiEcfClient
var receiver = dgii.Receiver; // IEcfReceiver
Uso como emisor
Todas las operaciones devuelven Result<T>. Un error esperado nunca lanza excepción: se revisa IsSuccess o Error. Cuando falla la DGII, Error es un DgiiApiError que trae Status, Messages (los mensajes de la DGII) y RawBody.
public sealed class FacturacionService(IDgiiEcfClient dgii)
{
public async Task<string?> EnviarAsync(Ecf factura, CancellationToken ct)
{
var firmado = dgii.SignDocument(factura); // o dgii.Sign(xml, "ECF")
if (firmado.IsFailure) return null;
// El token se obtiene y se renueva solo. Si la DGII responde 401, se reintenta una vez con un token nuevo.
// Por defecto el archivo se nombra {RNCEmisor}{eNCF}.xml.
var envio = await dgii.SendElectronicDocumentAsync(firmado.Value, cancellationToken: ct);
if (envio.IsFailure)
{
// envio.Error.Message trae el texto de la DGII, por ejemplo "e-NCF duplicado".
return null;
}
var estado = await dgii.GetTrackStatusAsync(envio.Value.TrackId!, ct);
return estado.IsSuccess ? estado.Value.Estado : null;
}
}
| Operación | Método |
|---|---|
| Autenticar (forzar) | AuthenticateAsync(buyerHost?) |
| Enviar e-CF a la DGII o a un receptor | SendElectronicDocumentAsync(signedXml, fileName?, buyerHost?) |
| Factura de consumo < RD$250,000 | CreateSignedRfce(signedEcf32) + SendSummaryAsync(rfce.SignedXml) |
| Aprobación comercial | SendCommercialApprovalAsync(signedAcecf, fileName?, buyerHost?) |
| Anulación de secuencias | VoidEncfAsync(signedAnecf, fileName?) |
| Estado por trackId | GetTrackStatusAsync(trackId) |
| Consulta de estado de un e-CF | InquiryStatusAsync(rncEmisor, encf, rncComprador?, codigoSeguridad?) |
| TrackIds de un e-NCF | GetTrackIdsAsync(rncEmisor, encf) |
| Directorio de facturadores | GetCustomerDirectoryAsync(rnc) (siempre devuelve una lista) |
| Consulta RFCE (solo producción) | GetSummaryInvoiceInquiryAsync(rncEmisor, encf, codigoSeguridad) |
| Estatus de servicios DGII | GetServicesStatusAsync(), GetMaintenanceWindowsAsync(), VerifyServiceStatusAsync() (requieren StatusApiKey) |
Factura de consumo (e-CF 32 menor de RD$250,000)
var ecf32 = dgii.SignDocument(facturaConsumo).Value; // guárdalo: es el documento completo
var rfce = dgii.CreateSignedRfce(ecf32).Value; // resumen firmado + código de seguridad
var respuesta = await dgii.SendSummaryAsync(rfce.SignedXml);
var qr = EcfTools.FcQrCodeUrl(rnc, encf, montoTotal, rfce.SecurityCode, DgiiEnvironment.Test);
Uso como receptor (estándar emisor-receptor)
Ejemplo con Minimal API:
app.MapGet("/fe/autenticacion/api/semilla", (IEcfReceiver r) => Results.Content(r.GenerateSeed(), "application/xml"));
app.MapPost("/fe/autenticacion/api/validacioncertificado", async (HttpRequest req, IEcfReceiver r) =>
{
var body = await new StreamReader(req.Body).ReadToEndAsync();
var archivo = r.ParseReceivedDocument(body, req.ContentType!);
var token = archivo.IsSuccess ? r.ValidateSignedSeed(archivo.Value.XmlContent) : null;
return token is { IsSuccess: true } ? Results.Ok(token.Value) : Results.Unauthorized();
});
app.MapPost("/fe/recepcion/api/ecf", async (HttpRequest req, IEcfReceiver r) =>
{
var auth = r.ValidateToken(req.Headers.Authorization.ToString());
if (auth.IsFailure || auth.Value.IsExpired) return Results.Unauthorized();
var body = await new StreamReader(req.Body).ReadToEndAsync();
var archivo = r.ParseReceivedDocument(body, req.ContentType!);
if (archivo.IsFailure) return Results.BadRequest(archivo.Error.Message);
// Rechaza por su cuenta los tipos 32, 41, 43, 45, 46 y 47 (código 1) y el RNC comprador ajeno (código 4).
var arecf = r.BuildSignedReceiptAcknowledgement(archivo.Value.XmlContent, receiverRnc: "130862346");
return arecf.IsSuccess ? Results.Content(arecf.Value, "application/xml") : Results.BadRequest(arecf.Error.Message);
});
Herramientas (EcfTools)
EcfTools es una clase estática y no necesita certificado ni conexión HTTP.
| Método | Qué hace |
|---|---|
GetSecurityCode(signedXml) |
Devuelve los primeros 6 caracteres del SignatureValue |
ConvertEcf32ToRfce(signedEcf32) |
Genera el RFCE sin firmar |
EcfQrCodeUrl(...) / FcQrCodeUrl(...) |
Construye las URL del QR con el mismo encoding que encodeURIComponent |
Serialize(doc) / Deserialize<T>(xml) |
Convierte entre documentos tipados y XML |
JsonToXml(json, round) / XmlToJson(xml) / NormalizeJsonCasing(json) |
Mantiene compatibles los payloads JSON del paquete Node |
SetXmlValue, ExtractXmlFromBody, CurrentFormattedDateTime |
Utilidades de XML y fecha |
Para verificar la firma de un documento recibido, usa el handler IValidateDocumentSignatureHandler desde el contenedor de DI.
Arquitectura
src/
core/domain DgiiEcf.Domain Result/Error, value objects (Rnc, Encf, SecurityCode), documentos tipados
core/app DgiiEcf.Application Features/<Módulo>/<Caso de uso> (Command/Query + Handler + Contracts), contratos
infrastructure/signing DgiiEcf.Signing Certificado .p12, XML-DSig, verificación, JWT RS256
infrastructure/externalservices DgiiEcf.ExternalServices HttpClient tipado, endpoints DGII, errores DGII
packaging/sdk DgiiEcf AddDgiiEcf(), fachadas, paquete NuGet único
srcTest/ espejo 1:1 de src/ (xUnit)
Diferencias con el paquete Node
- Token: en Node el token era global y compartido. Aquí se guarda por audiencia (la DGII o cada receptor), se renueva un minuto antes de expirar y se reintenta una vez ante un 401. Puedes reemplazar
IAccessTokenStorepor un store distribuido. - Validación de la semilla (receptor): Node solo comparaba el digest. Aquí también se verifica la firma criptográficamente.
- Verificación de firmas: acepta documentos firmados sin espacios en blanco y guardados indentados, como hacen las herramientas de la DGII. No valida la vigencia ni la cadena del certificado.
- Conversión a RFCE:
- funciona con un solo
ImpuestoAdicional(en Node fallaba); - rechaza documentos que no son tipo 32 o que llegan a RD$250,000 o más.
- funciona con un solo
- Certificado
.p12: se usa el certificado que tiene la clave privada, no el primero de la bolsa. - Servicio de estatus: usa
https://statusecf.dgii.gov.do/api/estatusservicios/...sin el segmento de ambiente que agregaba Node. - JSON ⇄ XML: los números del JSON se escriben tal como vienen (
637.20se mantiene como637.20). Node los normalizaba a637.2.
Desarrollo
dotnet test DgiiEcf.slnx
Las pruebas contra TesteCF se saltan salvo que definas DGII_CERT_PATH, DGII_CERT_PASSWORD y DGII_RNC_EMISOR.
dotnet pack src/packaging/sdk -c Release
Versionado y publicación
La versión del paquete sale de los tags de git mediante MinVer. El tag v1.2.3 produce la versión 1.2.3. Los commits que no tienen tag generan versiones preliminares, por ejemplo 1.2.4-alpha.0.3.
Hay dos workflows:
CI (
.github/workflows/ci.yml): en cada push y PR amastercompila, ejecuta las pruebas en net8.0 y net10.0, y deja el.nupkgcomo artefacto.Publish (
.github/workflows/publish.yml): se dispara de dos formas:- desde Actions → Publish → Run workflow, eligiendo
patch,minoromajor, con lo que crea el siguiente tag por su cuenta; - empujando un tag a mano, por ejemplo
git tag v1.2.3 && git push origin v1.2.3.
En los dos casos compila, prueba, empaqueta, publica en NuGet.org y crea el GitHub Release.
- desde Actions → Publish → Run workflow, eligiendo
La publicación en NuGet.org usa Trusted Publishing, sin API keys guardadas en el repositorio. La política de nuget.org debe coincidir con estos valores: owner dev-fcastro, repositorio dgii-ecf, workflow publish.yml, patrón DgiiEcf* y usuario fcastrodev.
Licencia
MIT. Basado en dgii-ecf de Victor Santos (MIT).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 8.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- System.Security.Cryptography.Xml (>= 10.0.12)
-
net8.0
- Microsoft.Extensions.DependencyInjection (>= 8.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- System.Security.Cryptography.Xml (>= 8.0.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 57 | 9/23/2026 |