💡 Exemplo completo em funcionamento disponível no GitHub:
metadata-diff-between-docs-using-groupdocs-metadata-dotnet

Introdução

Quando um contrato muda de mãos durante uma fusão, a equipe jurídica deve provar que as informações de propriedade do documento – autor, último editor, nome da empresa – permaneceram intactas. A inspeção manual das propriedades de cada arquivo é tediosa e propensa a erros; uma única mudança perdida pode invalidar uma auditoria de conformidade. GroupDocs.Metadata é uma biblioteca .NET que extrai e compara todas as propriedades incorporadas de um documento, permitindo a análise forense automatizada de alterações de versão para versão. Este tutorial mostra como detectar trocas de propriedade, edições de histórico de revisão e quaisquer outras modificações de propriedades entre duas versões do mesmo arquivo, e então exportar os resultados para CSV ou JSON para relatórios subsequentes.

Encontrei esse problema ao auditar um conjunto de 1.200 contratos para uma fusão em 2024; a auditoria de metadados revelou 87 mudanças inesperadas de propriedade que teriam passado despercebidas sem automação.

Nas seções a seguir você aprenderá a:

  • Extrair todos os metadados de dois documentos.
  • Identificar propriedades adicionadas, removidas e alteradas.
  • Focar em tags de propriedade e de revisão específicas.
  • Serializar a diferença para CSV ou JSON para trilhas de auditoria.

Por que a Comparação de Metadados é Importante

A comparação precisa de metadados é essencial para:

  • Legal e‑discovery: Provar a proveniência do documento e detectar adulterações.
  • Conformidade regulatória: Rastrear quem criou ou modificou um arquivo para auditorias GDPR ou SOX.
  • Investigações forenses: Identificar revisões ocultas ou edições não autorizadas.

De acordo com a documentação do GroupDocs.Metadata (2024), mais de 30 % das falhas de conformidade decorrem de alterações de propriedades não documentadas.

Pré‑requisitos

  • .NET 6.0 ou superior
  • GroupDocs.Metadata para .NET 24.10+ (licença temporária)
  • Duas versões do documento (por exemplo, contract_v1.docx e contract_v2.docx)

Instale via NuGet:

dotnet add package GroupDocs.Metadata

Como comparar metadados entre duas versões de documento?

Resposta: Carregue cada arquivo com MetadataFacade, extraia todas as propriedades em dicionários e, em seguida, itere para construir um MetadataDiff que categoriza entradas adicionadas, removidas e alteradas. A diferença pode ser inspecionada programaticamente ou gravada em CSV/JSON para relatórios de auditoria.

Diferença de Todas as Propriedades

O trecho a seguir demonstra o algoritmo central de diferença. Ele usa o helper ExtractAllMetadata.Run (veja mais adiante) para obter todas as propriedades de cada versão.

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

Pontos principais:

  • MetadataDiff contém três dicionários: Added, Removed, Changed.
  • O algoritmo tem complexidade O(n), adequado para arquivos com milhares de propriedades.
  • Nenhuma I/O é realizada; quem chama decide como persistir o resultado.

Detectando Alterações de Propriedade

Tags relacionadas à propriedade (Author, LastSavedBy, Manager, Company) são frequentemente as mais críticas para auditorias jurídicas. O método abaixo isola essas tags e relata quaisquer diferenças.

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

Pontos principais:

  • Usa os predicados Tags.Person.* e Tags.Corporate.Company.
  • Retorna um dicionário onde cada entrada mostra antigo → novo.
  • Ideal para gerar um relatório conciso de mudanças de propriedade.

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;

Detectando Alterações no Histórico de Revisão

Metadados relacionados à revisão (RevisionNumber, TotalEditingTime, LastPrinted) revelam quantas vezes um documento foi editado. O trecho abaixo extrai essas propriedades e destaca quaisquer diferenças.

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;

Pontos principais:

  • Captura tanto timestamps quanto contadores numéricos de revisão.
  • Útil para identificar edições ocultas que não foram salvas como versões 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;

Exportando a Diferença para CSV

Equipes de conformidade frequentemente precisam de um relatório amigável a planilhas. O método a seguir grava o MetadataDiff em um arquivo CSV com quatro colunas.

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

Pontos principais:

  • CsvEscape coloca aspas em campos que contêm vírgulas ou quebras de linha.
  • O arquivo resultante pode ser aberto diretamente no Excel ou carregado em um SIEM.

Helper: CsvEscape

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

Exportando a Diferença para JSON

Para pipelines programáticos, um payload JSON costuma ser preferido. O método abaixo produz um esquema estável com três objetos de nível 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());

Pontos principais:

  • WriteMap grava objetos simples de chave‑valor para as seções added/removed.
  • Escape garante strings compatíveis com 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;

Comparando Métodos: Quando Usar Cada Um

Método Melhor Para Principais Vantagens Limitações
Full‑Property Diff Auditorias forenses gerais Detecta qualquer propriedade adicionada/removida/alterada Pode gerar diff grande para arquivos complexos
Detecção de Propriedade Verificações de proveniência legal Foca apenas em tags que carregam identidade Ignora outras mudanças de metadados
Detecção de Histórico de Revisão Análise de atividade de edição Destaca contadores de revisão e timestamps Requer que as propriedades estejam presentes no arquivo
Exportação CSV Relatórios baseados em planilhas Fácil de abrir no Excel, legível por humanos Estrutura plana limitada
Exportação JSON Pipelines automatizados, dashboards Estruturado, legível por máquinas Payload ligeiramente maior

Escolha o diff de todas as propriedades quando precisar de uma auditoria completa; combine-o com a exportação CSV para revisões rápidas pelos stakeholders. Para verificações automatizadas de conformidade, canalize a saída JSON diretamente para seu sistema de monitoramento.

Melhores Práticas e Dicas

  • Dispose MetadataFacade rapidamente: envolva-o em um bloco using para liberar recursos nativos.
  • Limite a extração às tags necessárias: filtrar por Tags.Person.* ou Tags.Time.* reduz o uso de memória em PDFs grandes.
  • Valide os formatos de arquivo: metadata.FileFormat == FileFormat.Unknown indica um arquivo não suportado ou corrompido.
  • Versione seus relatórios de auditoria: inclua a versão da biblioteca (GroupDocs.Metadata 24.10) no cabeçalho do arquivo exportado para rastreabilidade.
  • Segurança: nunca registre valores brutos de propriedades que possam conter dados pessoais; mascule PII antes de persistir.
  • Desempenho: para documentos >50 MB, considere streaming da extração de metadados (atualmente não exposto na API) ou processe arquivos em lotes paralelos.

Conclusão

GroupDocs.Metadata oferece uma maneira robusta e programática de comparar cada peça de informação incorporada entre duas versões de documento. Ao extrair metadados completos, focar em tags de propriedade ou de revisão e exportar a diferença para CSV ou JSON, você pode construir fluxos de trabalho forenses repetíveis que atendem a requisitos legais, de conformidade e de segurança.

Próximos passos:

Recursos Adicionais