💡 Ejemplo completo disponible en GitHub:
metadata-diff-between-docs-using-groupdocs-metadata-dotnet
Introducción
Cuando un contrato cambia de manos durante una fusión, el equipo legal debe demostrar que la información de propiedad del documento — autor, último editor, nombre de la empresa — se mantuvo intacta. La inspección manual de las propiedades de cada archivo es tediosa y propensa a errores; un solo cambio pasado por alto puede invalidar una auditoría de cumplimiento. GroupDocs.Metadata es una biblioteca .NET que extrae y compara cada propiedad incrustada de un documento, permitiendo un análisis forense automatizado de los cambios de versión a versión. Este tutorial muestra cómo detectar intercambios de propiedad, ediciones del historial de revisiones y cualquier otra modificación de propiedades entre dos versiones del mismo archivo, y luego exportar los resultados a CSV o JSON para informes posteriores.
Me encontré con este problema al auditar un conjunto de 1.200 contratos para una fusión en 2024; la auditoría de metadatos reveló 87 cambios de propiedad inesperados que habrían sido invisibles sin automatización.
En las siguientes secciones aprenderá a:
- Extraer todos los metadatos de dos documentos.
- Identificar propiedades añadidas, eliminadas y modificadas.
- Centrarse en etiquetas específicas de propiedad y de revisión.
- Serializar la diferencia a CSV o JSON para rastros de auditoría.
Por qué la comparación de metadatos importa
La comparación precisa de metadatos es esencial para:
- Legal e‑discovery: Demostrar la procedencia del documento y detectar manipulaciones.
- Cumplimiento regulatorio: Rastrear quién creó o modificó un archivo para auditorías GDPR o SOX.
- Investigaciones forenses: Detectar revisiones ocultas o ediciones no autorizadas.
Según la documentación de GroupDocs.Metadata (2024), más del 30 % de los fallos de cumplimiento se deben a cambios de propiedades no documentados.
Requisitos previos
- .NET 6.0 o posterior
- GroupDocs.Metadata para .NET 24.10+ (licencia temporal)
- Dos versiones del documento (p. ej.,
contract_v1.docxycontract_v2.docx)
Instale vía NuGet:
dotnet add package GroupDocs.Metadata
¿Cómo comparo los metadatos entre dos versiones de un documento?
Respuesta: Cargue cada archivo con MetadataFacade, extraiga todas las propiedades en diccionarios y luego itere para construir un MetadataDiff que categorice las entradas añadidas, eliminadas y modificadas. La diferencia puede inspeccionarse programáticamente o escribirse a CSV/JSON para informes de auditoría.
Diferencia de propiedades completa
El siguiente fragmento muestra el algoritmo central de diferencia. Utiliza el ayudante ExtractAllMetadata.Run (ver más adelante) para extraer cada propiedad de cada versión.
// Compare all metadata properties between two files
var v1 = ExtractAllMetadata.Run(pathV1);
var v2 = ExtractAllMetadata.Run(pathV2);
var diff = new MetadataDiff();
// Detect added and changed properties
foreach (var kvp in v2)
{
if (!v1.ContainsKey(kvp.Key))
{
diff.Added[kvp.Key] = kvp.Value; // New property in v2
}
else if (v1[kvp.Key] != kvp.Value)
{
diff.Changed[kvp.Key] = (v1[kvp.Key], kvp.Value); // Value changed
}
}
// Detect removed properties
foreach (var kvp in v1)
{
if (!v2.ContainsKey(kvp.Key))
{
diff.Removed[kvp.Key] = kvp.Value; // Property missing in v2
}
}
return diff;
Puntos clave:
MetadataDiffcontiene tres diccionarios:Added,Removed,Changed.- El algoritmo se ejecuta en tiempo O(n), adecuado para archivos con miles de propiedades.
- No se realiza I/O; los llamadores deciden cómo persistir el resultado.
Detección de cambios de propiedad
Las etiquetas relacionadas con la propiedad (Author, LastSavedBy, Manager, Company) suelen ser las más críticas para auditorías legales. El método a continuación aísla esas etiquetas y reporta cualquier diferencia.
// Get ownership‑related properties from each version
var v1Values = GetOwnershipProperties(pathV1);
var v2Values = GetOwnershipProperties(pathV2);
var changes = new Dictionary<string, (string, string)>();
var allKeys = new HashSet<string>(v1Values.Keys);
foreach (var key in v2Values.Keys) allKeys.Add(key);
foreach (var key in allKeys)
{
var oldV = v1Values.TryGetValue(key, out var o) ? o : "<missing>";
var newV = v2Values.TryGetValue(key, out var n) ? n : "<missing>";
if (oldV != newV)
{
changes[key] = (oldV, newV);
}
}
return changes;
Puntos clave:
- Utiliza los predicados
Tags.Person.*yTags.Corporate.Company. - Devuelve un diccionario donde cada entrada muestra valores antiguo → nuevo.
- Ideal para generar un informe conciso de cambios de propiedad.
Helper: GetOwnershipProperties
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
using (var metadata = new MetadataFacade(path))
{
if (metadata.FileFormat == FileFormat.Unknown) return dict;
var props = metadata.FindProperties(p =>
p.Tags.Contains(Tags.Person.Creator) ||
p.Tags.Contains(Tags.Person.Editor) ||
p.Tags.Contains(Tags.Person.Manager) ||
p.Tags.Contains(Tags.Corporate.Company));
foreach (var p in props)
{
dict[p.Name] = p.InterpretedValue?.ToString() ?? p.Value?.ToString() ?? string.Empty;
}
}
return dict;
Detección de cambios en el historial de revisiones
Los metadatos relacionados con revisiones (RevisionNumber, TotalEditingTime, LastPrinted) revelan cuántas veces se editó un documento. El fragmento a continuación extrae esas propiedades y resalta cualquier diferencia.
var v1 = GetRevisionProperties(pathV1);
var v2 = GetRevisionProperties(pathV2);
var changes = new Dictionary<string, (string, string)>();
var allKeys = new HashSet<string>(v1.Keys);
foreach (var k in v2.Keys) allKeys.Add(k);
foreach (var key in allKeys)
{
var oldV = v1.TryGetValue(key, out var o) ? o : "<missing>";
var newV = v2.TryGetValue(key, out var n) ? n : "<missing>";
if (oldV != newV)
{
changes[key] = (oldV, newV);
}
}
return changes;
Puntos clave:
- Captura tanto marcas de tiempo como contadores numéricos de revisión.
- Útil para detectar ediciones ocultas que no se guardaron como versiones separadas.
Helper: GetRevisionProperties
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
using (var metadata = new MetadataFacade(path))
{
if (metadata.FileFormat == FileFormat.Unknown) return dict;
var props = metadata.FindProperties(p =>
p.Tags.Contains(Tags.Time.Modified) ||
p.Tags.Contains(Tags.Time.Created) ||
p.Tags.Contains(Tags.Time.Printed) ||
p.Name != null && (p.Name.Contains("Revision") || p.Name.Contains("EditTime") || p.Name.Contains("EditingTime")));
foreach (var p in props)
{
dict[p.Name] = p.InterpretedValue?.ToString() ?? p.Value?.ToString() ?? string.Empty;
}
}
return dict;
Exportar la diferencia a CSV
Los equipos de cumplimiento a menudo necesitan un informe amigable para hojas de cálculo. El siguiente método escribe el MetadataDiff en un archivo CSV con cuatro columnas.
var sb = new StringBuilder();
sb.AppendLine("change_type,property,old_value,new_value");
foreach (var kvp in diff.Added)
{
sb.AppendLine($"added,{CsvEscape(kvp.Key)},,{CsvEscape(kvp.Value)}");
}
foreach (var kvp in diff.Removed)
{
sb.AppendLine($"removed,{CsvEscape(kvp.Key)},{CsvEscape(kvp.Value)},");
}
foreach (var kvp in diff.Changed)
{
sb.AppendLine($"changed,{CsvEscape(kvp.Key)},{CsvEscape(kvp.Value.OldValue)},{CsvEscape(kvp.Value.NewValue)}");
}
File.WriteAllText(outputPath, sb.ToString());
Puntos clave:
CsvEscapecita de forma segura los campos que contienen comas o saltos de línea.- El archivo resultante puede abrirse directamente en Excel o cargarse en un SIEM.
Helper: CsvEscape
if (string.IsNullOrEmpty(s)) return string.Empty;
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;
Exportar la diferencia a JSON
Para canalizaciones programáticas, a menudo se prefiere una carga JSON. El método a continuación produce un esquema estable con tres objetos de nivel superior.
var sb = new StringBuilder();
sb.AppendLine("{");
sb.AppendLine(" \"added\": {");
WriteMap(sb, diff.Added);
sb.AppendLine(" },");
sb.AppendLine(" \"removed\": {");
WriteMap(sb, diff.Removed);
sb.AppendLine(" },");
sb.AppendLine(" \"changed\": {");
var changedItems = 0;
foreach (var kvp in diff.Changed)
{
var comma = ++changedItems < diff.Changed.Count ? "," : string.Empty;
sb.AppendLine($" \"{Escape(kvp.Key)}\": {{ \"from\": \"{Escape(kvp.Value.OldValue)}\", \"to\": \"{Escape(kvp.Value.NewValue)}\" }}{comma}");
}
sb.AppendLine(" }");
sb.AppendLine("}");
File.WriteAllText(outputPath, sb.ToString());
Puntos clave:
WriteMapescribe objetos clave‑valor simples para las secciones added/removed.Escapegarantiza cadenas compatibles con JSON.
Helper: WriteMap & Escape
var i = 0;
foreach (var kvp in map)
{
var comma = ++i < map.Count ? "," : string.Empty;
sb.AppendLine($" \"{Escape(kvp.Key)}\": \"{Escape(kvp.Value)}\"{comma}");
}
return s?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? string.Empty;
Comparación de métodos: cuándo usar cada uno
| Método | Mejor para | Ventajas clave | Limitaciones |
|---|---|---|---|
| Full‑Property Diff | Auditorías forenses generales | Detecta cualquier propiedad añadida, eliminada o modificada | Puede generar una diferencia grande para archivos complejos |
| Ownership Detection | Verificaciones de procedencia legal | Se centra solo en etiquetas que llevan identidad | Ignora otros cambios de metadatos |
| Revision History Detection | Análisis de actividad de edición | Resalta contadores de tiempo y de revisión | Requiere que las propiedades estén presentes en el archivo |
| CSV Export | Informes basados en hojas de cálculo | Fácil de abrir en Excel, legible por humanos | Limitado a estructura plana |
| JSON Export | Canalizaciones automatizadas, paneles | Estructurado, legible por máquinas | Carga ligeramente mayor |
Elija la diferencia de propiedades completa cuando necesite una auditoría exhaustiva; combínela con la exportación CSV para revisiones rápidas de los interesados. Para verificaciones de cumplimiento automatizadas, canalice la salida JSON directamente a su sistema de monitoreo.
Mejores prácticas y consejos
- Dispose
MetadataFacaderápidamente: envuélvalo en un bloqueusingpara liberar recursos nativos. - Limite la extracción a las etiquetas necesarias: filtrar por
Tags.Person.*oTags.Time.*reduce el uso de memoria para PDFs grandes. - Valide los formatos de archivo:
metadata.FileFormat == FileFormat.Unknownindica un archivo no compatible o corrupto. - Versione sus informes de auditoría: incluya la versión de la biblioteca (
GroupDocs.Metadata 24.10) en el encabezado del archivo exportado para trazabilidad. - Seguridad: nunca registre valores de propiedades sin procesar que puedan contener datos personales; enmascare la información de identificación personal (PII) antes de persistir.
- Rendimiento: para documentos >50 MB, considere transmitir la extracción de metadatos (actualmente no expuesta en la API) o procesar archivos en lotes paralelos.
Conclusión
GroupDocs.Metadata ofrece una forma robusta y programática de comparar cada pieza de información incrustada entre dos versiones de un documento. Al extraer metadatos completos, centrarse en etiquetas de propiedad o de revisión, y exportar la diferencia a CSV o JSON, puede crear flujos de trabajo forenses repetibles que cumplan con los requisitos legales, de cumplimiento y de seguridad.
Próximos pasos:
- Explore la lista completa de etiquetas compatibles para adaptar su diferencia a necesidades regulatorias específicas.
- Aprenda a comparar múltiples documentos simultáneamente (referencia de API).
- Consulte proyectos de ejemplo adicionales en GitHub para escenarios de procesamiento por lotes (Repositorio de ejemplos).