.Net serialización binaria sin referencia al ensamblaje con el tipo de fuente o cómo negociar con BinaryFormatter

En este artículo, compartiré la experiencia de la serialización de tipo binario entre ensamblajes, sin referencia mutua. Al final resultó que, hay casos reales y "legítimos" cuando necesita deserializar datos sin tener un enlace al ensamblado donde se declara. En el artículo hablaré sobre el escenario en el que se requirió, describiré el método de solución y también hablaré sobre los errores intermedios cometidos durante la búsqueda

Introduccion Declaración del problema.


Cooperamos con una gran corporación que trabaja en el campo de la geología. Históricamente, la corporación ha escrito software muy diferente para trabajar con datos provenientes de diferentes tipos de equipos + análisis de datos + pronósticos. Por desgracia, todo este software está lejos de ser siempre "amigable" entre sí, y la mayoría de las veces es amigable. Para consolidar de alguna manera la información, ahora se está creando un portal web, donde diferentes programas cargan sus datos en forma de xml. Y el portal está intentando crear una vista más-menos-completa. Un matiz importante: dado que los desarrolladores del portal no son fuertes en las áreas temáticas de cada aplicación, cada equipo proporcionó un módulo analizador / convertidor de datos desde su xml a las estructuras de datos del portal.

Trabajo en un equipo desarrollando una de las aplicaciones y escribimos con bastante facilidad un mecanismo de exportación para nuestra parte de los datos. Pero aquí, el analista de negocios decidió que el portal central necesitaba uno de los informes que nuestro programa estaba creando. Aquí es donde apareció el primer problema: el informe se crea de nuevo cada vez y los resultados no se guardan en ninguna parte.
"¡Así que guárdalo!" El lector probablemente pensará. Yo también lo pensaba, pero estaba muy decepcionado con el requisito de que el informe ya se haya creado para los datos descargados. Nada que hacer: necesita transferir la lógica.

Etapa 0. Refactorización. Nada auguraba problemas


Se decidió separar la lógica de compilación del informe (de hecho, esta es una etiqueta de 4 columnas, pero la lógica es un vagón y un carro grande) en una clase separada e incluir el archivo con esta clase como referencia en el ensamblador del analizador. Por esto nosotros:

  1. Evitar la copia directa
  2. Protección contra discrepancias de versión

Separar la lógica en una clase separada no es una tarea difícil. Pero entonces no todo fue tan optimista: el algoritmo se basó en objetos comerciales, cuya transferencia no se ajustaba a nuestro concepto. Tuve que reescribir los métodos para que solo acepten tipos simples y los operen. No siempre fue simple y, en algunos lugares, requería soluciones, cuya belleza seguía en duda, pero en general, se obtuvo una solución confiable sin muletas obvias.

Hubo un detalle que, como saben, a menudo sirve como un refugio acogedor para el demonio: heredamos un enfoque extraño de generaciones anteriores de desarrolladores, según el cual algunos de los datos necesarios para construir un informe se almacenan en la base de datos como objetos .Net serializados en binario ( preguntas "¿por qué?", ​​"kaaak?", etc., por desgracia, quedarán sin respuesta debido a la falta de destinatarios). Y en la entrada de los cálculos, nosotros, por supuesto, debemos deserializarlos.

Estos tipos, de los que era imposible deshacerse, también los incluimos "por referencia", especialmente porque no eran complicados.

Etapa 1. Deserialización. Recuerda el nombre completo del tipo


Después de hacer las manipulaciones anteriores y realizar una ejecución de prueba, inesperadamente recibí un error de tiempo de ejecución que
[A] Namespace.TypeA no se puede transmitir a [B] Namespace.TypeA. El tipo A se origina en 'Assembley.Application, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null' en el contexto 'Default' at location '...'. El tipo B se origina en 'Assmbley.Portal, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null' en el contexto 'Default' en la ubicación ''.
Los primeros enlaces de Google me dijeron que el hecho es que BinaryFormatter escribe no solo datos, sino que también escribe información en la secuencia de salida, lo cual es lógico. Y teniendo en cuenta que el nombre completo del tipo contiene el ensamblado en el que se declara, la imagen de lo que intenté deserializar un tipo es completamente diferente desde el punto de vista de .Net

Después de rascarme la cabeza, tomé una decisión obvia, pero desafortunadamente cruel, de reemplazar un tipo específico de Tipo A durante la deserialización dinámica . Todo funcionó. Los resultados del informe convergieron de arriba a abajo, las pruebas en el servidor de compilación pasaron. Con una sensación de logro, enviamos la tarea a los evaluadores.

Etapa 2. La principal. Serialización entre ensamblajes


El cálculo llegó rápidamente en forma de errores registrados por los probadores, que declararon que el analizador en el lado del portal cayó con la excepción de que no podía cargar el ensamblaje Assembley. Aplicación (ensamblaje de nuestra aplicación). Primer pensamiento: no limpié las referencias. Pero, no, todo está bien, nadie se refiere. Intento ejecutarlo nuevamente en el sandbox, todo funciona. Comienzo a sospechar un error de compilación, pero aquí se me ocurre una idea que no me agrada: cambio la ruta de salida del analizador a una carpeta separada y no al directorio bin compartido de la aplicación. Y listo, me sale la excepción descrita. El análisis de Stectrace confirma conjeturas vagas: la deserialización está cayendo.

La conciencia fue rápida y dolorosa: reemplazar un tipo específico con dinámico no cambió nada, BinaryFormatter todavía creó un tipo a partir de un ensamblaje externo, solo cuando el ensamblaje con el tipo estaba cerca, el tiempo de ejecución lo cargó naturalmente y cuando el ensamblaje desapareció. Obtenemos un error.

Había una razón para estar triste. Pero googlear dio esperanza en la forma de la clase SerializationBinder . Al final resultó que, le permite determinar el tipo en que se deserializan nuestros datos. Para hacer esto, cree un heredero y defina el siguiente método en él.

public abstract Type BindToType(String assemblyName, String typeName); 

en el que puede devolver cualquier tipo para las condiciones dadas.
La clase BinaryFormatter tiene una propiedad Binder donde puede inyectar su implementación.

Parece que no hay problema. Pero nuevamente, los detalles permanecen (ver arriba).

Primero, debe procesar las solicitudes para todos los tipos (y también estándar).
Aquí se encontró una opción de implementación interesante en Internet, pero están tratando de usar la carpeta predeterminada de BinaryFormatter, en forma de construcción

 var defaultBinder = new BinaryFormatter().Binder 

Pero, de hecho, la propiedad Binder es nula por defecto. Un análisis del código fuente mostró que dentro de BinaryFormatter, si Binder está marcado, si es así, sus métodos se llaman, si no, se utiliza la lógica interna, que finalmente se reduce a

  var assembly = Assembly.Load(assemblyName); return FormatterServices.GetTypeFromAssembly(assembly, typeName); 

Sin más preámbulos, repetí la misma lógica en mí mismo.

Esto es lo que sucedió en la primera implementación.

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

Es decir se verifica si el espacio de nombres pertenece al proyecto (devolvemos el tipo del dominio actual, si el tipo de sistema) lo cargamos desde el ensamblado correspondiente

Se ve lógico. Comenzamos a probar: nuestro tipo viene, lo reemplazamos, se crea. ¡Hurra! Cadena viene - vamos a lo largo de la rama con la carga del conjunto. Funciona! Abrir champán virtual ...

Pero aquí ... Un diccionario viene con elementos de tipos de usuario: dado que este es un tipo de sistema, entonces ... obviamente, estamos tratando de cargarlo desde el ensamblado, pero dado que los elementos que tiene son nuestros tipos, además, con calificación completa (ensamblaje, versión, clave ), luego caemos de nuevo. (Debería haber una sonrisa triste).

Claramente, debe cambiar el nombre de entrada del tipo, sustituyendo enlaces al ensamblaje deseado. Realmente esperaba que para el nombre del tipo, haya un análogo de la clase AssemblyName , pero no encontré nada similar. Escribir un analizador universal con reemplazo no es una tarea fácil. Después de una serie de experimentos, llegué a la siguiente solución: en el constructor estático, resto los tipos para reemplazar, y luego busco sus nombres en la línea con el nombre del tipo creado, y cuando lo encuentro, reemplazo el nombre del ensamblado

  /// <summary> /// The types that may be changed to local /// </summary> protected static IEnumerable<Type> _changedTypes; static MyBinder() { var executingAssembly = Assembly.GetCallingAssembly(); var name = executingAssembly.GetName().Name; _changedTypes = executingAssembly.GetTypes().Where(t => t.Namespace != null && !t.Namespace.Contains(name) && !t.Name.StartsWith("<")); //!t.Namespace.Contains(name) - .     ,         // "<'      -     } private static string CorrectTypeName(string name) { foreach (var changedType in _changedTypes) { var ind = name.IndexOf(changedType.FullName); if (ind != -1) { var endIndex = name.IndexOf("PublicKeyToken", ind) ; if (endIndex != -1) { endIndex += +"PublicKeyToken".Length + 1; while (char.IsLetterOrDigit(name[endIndex++])) { } var sb = new StringBuilder(); sb.Append(name.Substring(0, ind)); sb.Append(changedType.AssemblyQualifiedName); sb.Append(name.Substring(endIndex-1)); name = sb.ToString(); } } } return name; } /// <summary> /// look up the type locally if the assembly-name is "NA" /// </summary> /// <param name="assemblyName"></param> /// <param name="typeName"></param> /// <returns></returns> public override Type BindToType(string assemblyName, string typeName) { typeName = CorrectTypeName(typeName); if (assemblyName.Contains("<ObligatoryPartOfNamespace>") || assemblyName.Equals("NA")) { var bindToType = Type.GetType(typeName); return bindToType; } else { var bindToType = LoadTypeFromAssembly(assemblyName, typeName); return bindToType; } } 

Como puede ver, comencé por el hecho de que PublicKeyToken es el último en la descripción del tipo. Quizás esto no sea 100% confiable, pero en mis pruebas no encontré casos en los que esto no sea así.

Por lo tanto, una línea de la forma
"System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType, Assembley.Application, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null], [System.Byte [], mscorlib, Version = 4.0.0.0, Cultura = neutral, PublicKeyToken = b77a5c561934e089]] »

se convierte en
"System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType, Assembley.Portal, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null], [System.Byte [], mscorlib, Version = 4.0.0.0, Cultura = neutral, PublicKeyToken = b77a5c561934e089]] »

Ahora todo finalmente funcionó "como un reloj". Hubo sutilezas técnicas menores: si recuerdas, los archivos que incluimos se incluyeron en el enlace desde la aplicación principal. Pero en la aplicación principal no se necesitan todos estos bailes. Por lo tanto, un mecanismo de compilación condicional del formulario

 BinaryFormatter binForm = new BinaryFormatter(); #if EXTERNAL_LIB binForm.Binder = new MyBinder(); #endif 

En consecuencia, en el ensamblaje del portal definimos la macro EXTERNAL_LIB, pero en la aplicación principal, no

"Digresión no lírica"


De hecho, en el proceso de codificación, para verificar rápidamente la solución, cometí un error de cálculo, que probablemente me costó un cierto número de células nerviosas: para empezar, simplemente codifiqué la sustitución de tipo para Dicitionary. Como resultado, después de la deserialización, resultó ser un diccionario vacío, que también se "bloqueó" al intentar realizar algunas operaciones con él. Ya estaba empezando a pensar que no podías engañar a BinaryFormatter , y comencé experimentos desesperados con un intento de escribir el heredero del Diccionario. Afortunadamente, paré casi a tiempo y volví a escribir un mecanismo de sustitución universal y, al implementarlo, me di cuenta de que crear un Diccionario no es suficiente para redefinir su tipo: aún debe cuidar los tipos para KeyValuePair <TKey, TValue>, Comparer, que también se solicitan a Aglutinante


Estas son las aventuras de serialización binaria. Estaría agradecido por los comentarios.

Source: https://habr.com/ru/post/es430646/


All Articles