💡 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.docxecontract_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:
MetadataDiffconté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.*eTags.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:
CsvEscapecoloca 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:
WriteMapgrava objetos simples de chave‑valor para as seções added/removed.Escapegarante 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
MetadataFacaderapidamente: envolva-o em um blocousingpara liberar recursos nativos. - Limite a extração às tags necessárias: filtrar por
Tags.Person.*ouTags.Time.*reduz o uso de memória em PDFs grandes. - Valide os formatos de arquivo:
metadata.FileFormat == FileFormat.Unknownindica 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:
- Explore a lista completa de tags suportadas para adaptar seu diff a necessidades regulatórias específicas.
- Aprenda a comparar múltiplos documentos simultaneamente (referência da API).
- Confira projetos de exemplo adicionais no GitHub para cenários de processamento em lote (Repositório de Exemplos).