💡 Esempio completo funzionante disponibile su GitHub:
metadata-diff-between-docs-using-groupdocs-metadata-dotnet

Introduzione

Quando un contratto cambia mano durante una fusione, il team legale deve dimostrare che le informazioni di proprietà del documento – autore, ultimo editore, nome dell’azienda – sono rimaste intatte. L’ispezione manuale delle proprietà di ogni file è noiosa e soggetta a errori; una singola modifica non rilevata può invalidare un audit di conformità. GroupDocs.Metadata è una libreria .NET che estrae e confronta ogni proprietà incorporata di un documento, consentendo un’analisi forense automatizzata delle modifiche da una versione all’altra. Questo tutorial mostra come rilevare scambi di proprietà, modifiche alla cronologia delle revisioni e qualsiasi altra modifica di proprietà tra due versioni dello stesso file, per poi esportare i risultati in CSV o JSON per report successivi.

Ho incontrato questo problema durante l’audit di un set di 1.200 contratti per una fusione del 2024; l’audit dei metadati ha rivelato 87 cambiamenti di proprietà inattesi che sarebbero stati invisibili senza automazione.

Nelle sezioni seguenti imparerai a:

  • Estrarre tutti i metadati da due documenti.
  • Identificare proprietà aggiunte, rimosse e modificate.
  • Concentrarti su tag relativi a proprietà e revisioni.
  • Serializzare il diff in CSV o JSON per le catene di audit.

Perché il Confronto dei Metadati è Importante

Il confronto accurato dei metadati è essenziale per:

  • Legal e‑discovery: Dimostrare la provenienza del documento e rilevare manomissioni.
  • Conformità normativa: Tracciare chi ha creato o modificato un file per audit GDPR o SOX.
  • Indagini forensi: Individuare revisioni nascoste o modifiche non autorizzate.

Secondo la documentazione di GroupDocs.Metadata (2024), oltre il 30 % dei fallimenti di conformità deriva da modifiche di proprietà non documentate.

Prerequisiti

  • .NET 6.0 o successivo
  • GroupDocs.Metadata per .NET 24.10+ (licenza temporanea)
  • Due versioni del documento (ad es. contract_v1.docx e contract_v2.docx)

Installa tramite NuGet:

dotnet add package GroupDocs.Metadata

Come confronto i metadati tra due versioni di documento?

Risposta: Carica ciascun file con MetadataFacade, estrai tutte le proprietà in dizionari, quindi itera per costruire un MetadataDiff che categorizza le voci aggiunte, rimosse e modificate. Il diff può essere ispezionato programmaticamente o scritto in CSV/JSON per i report di audit.

Diff di Tutte le Proprietà

Il frammento seguente dimostra l’algoritmo di diff principale. Utilizza l’helper ExtractAllMetadata.Run (vedi più avanti) per estrarre ogni proprietà da ciascuna versione.

// 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;

Punti chiave:

  • MetadataDiff contiene tre dizionari: Added, Removed, Changed.
  • L’algoritmo funziona in tempo O(n), adatto a file con migliaia di proprietà.
  • Non vengono eseguite operazioni I/O; spetta al chiamante decidere come persistere il risultato.

Rilevare le Modifiche di Proprietà

I tag relativi alla proprietà (Author, LastSavedBy, Manager, Company) sono spesso i più critici per gli audit legali. Il metodo qui sotto isola quei tag e segnala eventuali differenze.

// 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;

Punti chiave:

  • Utilizza i predicati Tags.Person.* e Tags.Corporate.Company.
  • Restituisce un dizionario in cui ogni voce mostra vecchio → nuovo valore.
  • Ideale per generare un report conciso delle modifiche di proprietà.

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;

Rilevare le Modifiche alla Cronologia delle Revisioni

I metadati relativi alle revisioni (RevisionNumber, TotalEditingTime, LastPrinted) rivelano quante volte un documento è stato modificato. Il frammento sotto estrae quelle proprietà e mette in evidenza eventuali scostamenti.

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;

Punti chiave:

  • Cattura sia timestamp sia contatori numerici di revisione.
  • Utile per individuare modifiche nascoste che non sono state salvate come versioni separate.

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;

Esportare il Diff in CSV

I team di conformità spesso hanno bisogno di un report adatto a fogli di calcolo. Il metodo seguente scrive il MetadataDiff in un file CSV con quattro colonne.

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());

Punti chiave:

  • CsvEscape quotizza in modo sicuro i campi contenenti virgole o interruzioni di riga.
  • Il file risultante può essere aperto direttamente in Excel o caricato in un SIEM.

Helper: CsvEscape

if (string.IsNullOrEmpty(s)) return string.Empty;
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
    return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;

Esportare il Diff in JSON

Per pipeline programmatiche, spesso è preferibile un payload JSON. Il metodo qui sotto produce uno schema stabile con tre oggetti di primo livello.

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());

Punti chiave:

  • WriteMap scrive semplici oggetti chiave‑valore per le sezioni aggiunte/rimosse.
  • Escape garantisce stringhe compatibili 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;

Confronto dei Metodi: Quando Usare Ciascuno

Metodo Ideale Per Vantaggi Principali Limitazioni
Full‑Property Diff Audit forense generali Rileva qualsiasi proprietà aggiunta/rimossa/modificata Può produrre diff molto grandi per file complessi
Ownership Detection Controlli di provenienza legale Si concentra solo sui tag legati all’identità Ignora altre modifiche di metadati
Revision History Detection Analisi dell’attività di editing Evidenzia contatori temporali e di revisione Richiede che le proprietà siano presenti nel file
CSV Export Report basati su fogli di calcolo Facile da aprire in Excel, leggibile da umano Struttura piatta limitata
JSON Export Pipeline automatizzate, dashboard Strutturato, leggibile da macchina Payload leggermente più grande

Scegli il diff a proprietà complete quando ti serve un audit esaustivo; combinalo con l’esportazione CSV per revisioni rapide da parte degli stakeholder. Per controlli di conformità automatizzati, indirizza direttamente l’output JSON al tuo sistema di monitoraggio.

Best Practice e Suggerimenti

  • Rilascia MetadataFacade subito: avvolgilo in un blocco using per liberare le risorse native.
  • Limita l’estrazione ai tag necessari: filtrare per Tags.Person.* o Tags.Time.* riduce l’uso di memoria per PDF di grandi dimensioni.
  • Valida i formati dei file: metadata.FileFormat == FileFormat.Unknown indica un file non supportato o corrotto.
  • Versiona i report di audit: includi la versione della libreria (GroupDocs.Metadata 24.10) nell’intestazione del file esportato per tracciabilità.
  • Sicurezza: non registrare valori grezzi di proprietà che possano contenere dati personali; maschera i PII prima di persistere.
  • Performance: per documenti > 50 MB, considera lo streaming dell’estrazione dei metadati (attualmente non esposto nell’API) o elabora i file in batch paralleli.

Conclusione

GroupDocs.Metadata offre un modo robusto e programmatico per confrontare ogni informazione incorporata tra due versioni di documento. Estrarre tutti i metadati, concentrarsi su tag di proprietà o di revisione e esportare il diff in CSV o JSON ti permette di costruire workflow forensi ripetibili che soddisfano requisiti legali, di conformità e di sicurezza.

Passi successivi:

Risorse Aggiuntive