Neste artigo, compartilharei a experiência de serialização de tipo binário entre assemblies, sem referência um ao outro. Como se viu, existem casos reais e "legítimos" em que você precisa desserializar dados sem ter um link para o assembly em que eles são declarados. No artigo, falarei sobre o cenário em que foi necessário, descreverei o método de solução e também falarei sobre erros intermediários cometidos no processo de busca
1. Introdução Declaração do problema
Cooperamos com uma grande corporação que trabalha no campo da geologia. Historicamente, a empresa escreveu um software muito diferente para trabalhar com dados provenientes de diferentes tipos de equipamento + análise de dados + previsão. Infelizmente, todo esse software está longe de ser sempre “amigável” um com o outro e, na maioria das vezes, nem um pouco amigável. Para, de alguma forma, consolidar as informações, agora está sendo criado um portal da Web, onde diferentes programas enviam seus dados na forma de xml. E o portal está tentando criar uma visão mais-menos-completa. Uma nuance importante: como os desenvolvedores do portal não são fortes nas áreas de assunto de cada aplicativo, cada equipe forneceu um módulo analisador / conversor de dados de seu xml para as estruturas de dados do portal.
Eu trabalho em uma equipe desenvolvendo um dos aplicativos e escrevemos com facilidade um mecanismo de exportação para nossos dados. Mas aqui, o analista de negócios decidiu que o portal central precisava de um dos relatórios que nosso programa estava construindo. Foi aqui que o primeiro problema apareceu: o relatório é construído novamente a cada vez e os resultados não são salvos em nenhum lugar.
"Então salve!" O leitor provavelmente pensará. Eu também pensava assim, mas fiquei seriamente desapontado com a exigência de que o relatório já fosse construído para os dados baixados. Nada a fazer - você precisa transferir a lógica.
Etapa 0. Refatoração. Nada com problemas
Foi decidido separar a lógica da criação do relatório (na verdade, esse é um rótulo de 4 colunas, mas a lógica é um vagão e um carrinho grande) em uma classe separada e incluir o arquivo com essa classe por referência no conjunto do analisador. Com isso, nós:
- Evite cópia direta
- Protegendo contra discrepâncias de versão
Separar a lógica em uma classe separada não é uma tarefa difícil. Mas nem tudo foi tão otimista: o algoritmo foi baseado em objetos de negócios, cuja transferência não se encaixava no nosso conceito. Eu tive que reescrever os métodos para que eles aceitassem apenas tipos simples e operassem com eles. Nem sempre era simples e, em alguns lugares, exigia decisões, cuja beleza permanecia em questão, mas, em geral, uma solução confiável foi obtida sem muletas óbvias.
Havia um detalhe que, como você sabe, geralmente serve como um refúgio acolhedor para o diabo: herdamos uma abordagem estranha das gerações anteriores de desenvolvedores, segundo a qual alguns dos dados necessários para criar um relatório são armazenados no banco de dados como objetos .Net serializados em binários ( as perguntas “por quê?”, “kaaak?”, etc., infelizmente, permanecerão sem resposta devido à falta de destinatários). E na entrada dos cálculos, é claro que devemos desserializá-los.
Esses tipos, dos quais era impossível se livrar, também incluímos "por referência", especialmente porque eles não eram complicados.
Etapa 1. Desserialização. Lembre-se do nome completo do tipo
Depois de fazer as manipulações acima e executar uma execução de teste, recebi inesperadamente um erro de tempo de execução que
[A] Namespace.TypeA não pode ser convertido em [B] Namespace.TypeA. O tipo A é originário de 'Assembley.Application, Versão = 1.0.0.0, Culture = neutral, PublicKeyToken = null' no contexto 'Padrão' no local '...'. O tipo B é originário de 'Assmbley.Portal, versão = 1.0.0.0, Cultura = neutra, PublicKeyToken = null' no contexto 'Padrão' no local ''.
Os primeiros links do Google me disseram que o BinaryFormatter grava não apenas dados, mas também digita informações no fluxo de saída, o que é lógico. E, levando em consideração que o nome completo do tipo contém a montagem na qual é declarado, a imagem do que tentei desserializar um tipo é completamente diferente do ponto de vista do .Net
Depois de coçar a cabeça, tomei uma decisão óbvia, mas, infelizmente, cruel, de substituir um tipo TypeA específico durante
a desserialização
dinâmica . Tudo funcionou. Os resultados do relatório convergiram de cima para baixo, os testes no servidor de compilação foram aprovados. Com uma sensação de realização, enviamos a tarefa aos testadores.
Etapa 2. O principal. Serialização entre montagens
O acerto de contas veio rapidamente na forma de bugs registrados pelos testadores, que afirmavam que o analisador no lado do portal caiu com a exceção de que não era possível carregar o assembly Assembley.Application (montagem do nosso aplicativo). Primeiro pensamento: não limpei as referências. Mas - não, está tudo bem, ninguém se refere. Eu tento executá-lo novamente na caixa de areia - tudo funciona. Começo a suspeitar de um erro de compilação, mas aqui surge uma idéia que não me agrada: altero o caminho de saída do analisador para uma pasta separada e não para o diretório bin compartilhado do aplicativo. E pronto - recebo a exceção descrita. A análise Stectrace confirma suposições vagas - a desserialização está caindo.
A conscientização foi rápida e dolorosa: a substituição de um tipo específico por dinâmico não mudou nada, o BinaryFormatter ainda criou um tipo a partir de uma montagem externa, somente quando a montagem com o tipo estava próxima, o tempo de execução o carregava naturalmente e quando a montagem desaparecia - temos um erro.
Havia um motivo para ficar triste. Mas pesquisar no Google deu esperança na forma da
classe SerializationBinder . Como se viu, permite determinar o tipo em que nossos dados são desserializados. Para fazer isso, crie um herdeiro e defina o seguinte método.
public abstract Type BindToType(String assemblyName, String typeName);
em que você pode retornar qualquer tipo para determinadas condições.
A classe BinaryFormatter possui uma propriedade
Binder na qual você pode injetar sua implementação.
Parece que não há problema. Mas, novamente, os detalhes permanecem (veja acima).
Primeiro, você deve processar solicitações para
todos os tipos (e padrão também).
Uma opção de implementação interessante foi encontrada na Internet aqui , mas eles estão tentando usar o fichário padrão do BinaryFormatter, na forma de uma construção
var defaultBinder = new BinaryFormatter().Binder
Mas, de fato, a propriedade Binder é nula por padrão. Uma análise do código fonte mostrou que, dentro do BinaryFormatter, se o Binder está marcado, em caso afirmativo, seus métodos são chamados, se não, é usada lógica interna, que finalmente se resume a
var assembly = Assembly.Load(assemblyName); return FormatterServices.GetTypeFromAssembly(assembly, typeName);
Sem mais delongas, repeti a mesma lógica em mim.
Aqui está o que aconteceu na primeira implementação
public class MyBinder : SerializationBinder { public override Type BindToType(string assemblyName, string typeName) { if (assemblyName.Contains("<ObligatoryPartOfNamespace>") ) { var bindToType = Type.GetType(typeName); return bindToType; } else { var bindToType = LoadTypeFromAssembly(assemblyName, typeName); return bindToType; } } private Type LoadTypeFromAssembly(string assemblyName, string typeName) { if (string.IsNullOrEmpty(assemblyName) || string.IsNullOrEmpty(typeName)) return null; var assembly = Assembly.Load(assemblyName); return FormatterServices.GetTypeFromAssembly(assembly, typeName); } }
I.e. é verificado se o espaço para nome pertence ao projeto - retornamos o tipo do domínio atual, se o tipo de sistema - carregamos do assembly correspondente
Parece lógico. Começamos a testar: nosso tipo vem - nós substituímos, ele é criado. Viva! String vem - nós seguimos o ramo com o carregamento da montagem. Isso funciona! Champanhe virtual aberto ...
Mas aqui ... Um dicionário vem com elementos de tipos de usuário: como esse é um tipo de sistema, então ... obviamente, estamos tentando carregá-lo da montagem, mas como os elementos que ela possui são nossos tipos, além disso, com qualificação completa (montagem, versão, chave ), então caímos novamente. (deve haver um sorriso triste).
Claramente, você precisa alterar o nome de entrada do tipo, substituindo os links na montagem desejada. Eu realmente esperava que, para o nome do tipo, houvesse um análogo da classe
AssemblyName , mas não encontrei nada parecido. Escrever um analisador universal com substituição não é uma tarefa fácil. Após uma série de experimentos, cheguei à seguinte solução: no construtor estático, subtraio os tipos a serem substituídos e, em seguida, procuro seus nomes na linha com o nome do tipo criado e, quando o encontro, substituo o nome do assembly
Como você pode ver, comecei pelo fato de que PublicKeyToken é o último na descrição do tipo. Talvez isso não seja 100% confiável, mas em meus testes não encontrei casos em que isso não fosse verdade.
Assim, uma linha do formulário
"System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType, Assembley.Application, Versão = 1.0.0.0, Cultura = neutra, PublicKeyToken = null], [System.Byte [], mscorlib, Versão = 4.0.0.0, Cultura = neutra, PublicKeyToken = b77a5c561934e089]] »
se transforma em
"System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType, Assembley.Portal, Versão = 1.0.0.0, Cultura = neutra, PublicKeyToken = null], [System.Byte [], mscorlib, Versão = 4.0.0.0, Cultura = neutra, PublicKeyToken = b77a5c561934e089]] »
Agora tudo finalmente funcionou "como um relógio". Havia pequenas sutilezas técnicas: se você se lembra, os arquivos que incluímos foram incluídos no link do aplicativo principal. Mas na aplicação principal, todas essas danças não são necessárias. Portanto, um mecanismo de compilação condicional do formulário
BinaryFormatter binForm = new BinaryFormatter(); #if EXTERNAL_LIB binForm.Binder = new MyBinder(); #endif
Assim, na montagem do portal, definimos a macro EXTERNAL_LIB, mas no aplicativo principal - não
"Digressão não lírica"
De fato, no processo de codificação, para verificar rapidamente a solução, fiz um erro de cálculo, o que provavelmente me custou um certo número de células nervosas: para iniciantes, eu apenas codifiquei a substituição de tipo para Dicitionary. Como resultado, após a desserialização, acabou sendo um dicionário vazio, que também "travou" ao tentar executar algumas operações com ele. Eu já estava começando a pensar que você não podia enganar o BinaryFormatter e comecei experiências desesperadas com a tentativa de escrever o herdeiro do Dictionary. Felizmente, parei quase a tempo e voltei a escrever um mecanismo de substituição universal e, implementando-o, percebi que para criar um Dicionário não basta redefinir seu tipo: você ainda precisa cuidar dos tipos de KeyValuePair <TKey, TValue>, Comparer, que também são solicitados a Fichário
Essas são as aventuras de serialização binária. Ficaria muito grato pelo feedback.