Cadenas de conexión de Entity Framework… Cuidado!

Hola a todos!

Hoy voy a comentar algo bastante curioso con lo que me he topado trabajando con Entity Framework. A modo de preámbulo, os diré que durante la ejecución de una aplicación, cada vez que instanciábamos un objeto de una clase que heredase de ObjectContext se lanzaban una serie de excepciones, cuyo número iba creciendo. Es decir, que simplemente invocando al constructor, sin conectarse aún a ningún sitio, podíamos ver cómo se lanzaban 2 excepciones. A la siguiente invocación del constructor (de la misma clase!) podían lanzarse 3… Es decir, el número de excepciones iba «in crescendo». Y aunque iban aumentando el número de excepciones por invocación de constructor, no había un patrón para poder determinar por qué iba aumentando ese número. O al menos el patrón no era aparente del todo (tranquilos que al final desenmascararemos el misterio).

Por otro lado, para agravar más aún la situación, los ObjectContext tenían una vida cortísima, y se creaban y destruían continuamente (arquitectura empresarial, acceso a datos desde componentes sin estado… bueno, os hacéis a la idea, ¿verdad?), con lo que el número de excepciones lanzadas era… enorme.

Como a estas alturas ya sabréis, en las cadenas de conexión de Entity Framework hay que indicar varios parámetros:

  • El proveedor de acceso a datos a utilizar
  • La cadena de conexión a base de datos (de las de ADO.NET de toda la vida) que usa el proveedor del punto anterior.
  • Dónde están los ficheros de metadatos.

Para crear cadenas de conexión mediante código, se recomienda utilizar la clase EntityConnectionStringBuilder, tal y como se indica aquí.

Bien… Pues la causa de las excepciones era el cómo indicábamos dónde están los metadatos. Los ficheros de metadatos los tenemos dentro de los ensamblados en los que tenemos definidas las entidades. Por lo tanto, la ubicación de los metadatos la estábamos indicando de la siguiente manera:

res://*/MyEntities.csdl|res://*/MyEntities.ssdl|res://*/MyEntities.msl

Que quiere decir: Busca en los recursos del ensamblado «*» (es decir, en todos los ensamblados), un recurso que se llame «MyEntities.csdl» (modelo conceptual), otro que se llame «MyEntities.ssdl» (modelo de almacenamiento) y otro que se llame «MyEntities.msl» (mappings).

En casos normales, la búsqueda de esos metadatos no debería dar ningún problema. La búsqueda de los metadatos (según el MSDN) se lleva a cabo de la siguiente forma:

  1. Busca el recurso indicado en el ensamblado que realiza la llamada
  2. Busca el recurso indicado en los ensamblados referenciados desde el ensamblado del punto anterior
  3. Busca el recurso indicado en los ensamblados que se encuentran en la carpeta de trabajo de la aplicación

Esta información no es incorrecta, pero es incompleta. La búsqueda de los recursos se lleva a cabo utilizando la misma táctica que cuando se debe localizar un ensamblado concreto. Es decir, que el tercer paso sería más bien así:

  • Busco en los ensamblados cargados en el dominio de aplicación
  • Busco en los ensamblados de la carpeta de trabajo actual
  • Busco en los ensamblados de la Global Assembly Cache (GAC).

Esto creo que es así por la excepción que me lanzaban los constructores de las clases derivadas de ObjectContext. La excepción en concreto era la siguiente:

Ocurrió NotSupportedException: No se admite el miembro invocado en un ensamblado dinámico.

Y la pila de llamadas era la siguiente:

mscorlib.dll!System.Reflection.Emit.AssemblyBuilder.GetManifestResourceNames() + 0x39 bytes
System.Data.Entity.dll!System.Data.Metadata.Edm.MetadataArtifactLoaderCompositeResource.GetManifestResourceNamesForAssembly(System.Reflection.Assembly assembly) + 0x24 bytes
System.Data.Entity.dll!System.Data.Metadata.Edm.MetadataArtifactLoaderCompositeResource.AssemblyContainsResource(System.Reflection.Assembly assembly, ref string resourceName = «MyEntities.csdl») + 0x22 bytes
System.Data.Entity.dll!System.Data.Metadata.Edm.MetadataArtifactLoaderCompositeResource.LoadResources(string assemblyName, string resourceName, System.Collections.Generic.ICollection<string> uriRegistry = Count = 0, System.Data.Metadata.Edm.MetadataArtifactAssemblyResolver resolver = {System.Data.Metadata.Edm.DefaultAssemblyResolver}) + 0x91 bytes
System.Data.Entity.dll!System.Data.Metadata.Edm.MetadataArtifactLoaderCompositeResource.CreateResourceLoader(string path, System.Data.Metadata.Edm.MetadataArtifactLoader.ExtensionCheck extensionCheck, string validExtension, System.Collections.Generic.ICollection<string> uriRegistry, System.Data.Metadata.Edm.MetadataArtifactAssemblyResolver resolver) + 0xba bytes
System.Data.Entity.dll!System.Data.Metadata.Edm.MetadataArtifactLoader.Create(string path, System.Data.Metadata.Edm.MetadataArtifactLoader.ExtensionCheck extensionCheck, string validExtension, System.Collections.Generic.ICollection<string> uriRegistry, System.Data.Metadata.Edm.MetadataArtifactAssemblyResolver resolver) + 0x40 bytes
System.Data.Entity.dll!System.Data.EntityClient.EntityConnection.SplitPaths(string paths) + 0x294 bytes
System.Data.Entity.dll!System.Data.EntityClient.EntityConnection.GetMetadataWorkspace(bool initializeAllCollections = false) + 0x95 bytes
System.Data.Entity.dll!System.Data.Objects.ObjectContext.RetrieveMetadataWorkspaceFromConnection() + 0x1b bytes
System.Data.Entity.dll!System.Data.Objects.ObjectContext.ObjectContext(System.Data.EntityClient.EntityConnection connection, bool isConnectionConstructor = false) + 0xad bytes
System.Data.Entity.dll!System.Data.Objects.ObjectContext.ObjectContext(string connectionString, string defaultContainerName = «MyEntities») + 0x1c bytes

Es decir… que fallaba en el método GetManifestResourceNames, quejándose de que ese método no está soportado cuando se invoca sobre un ensamblado dinámico. El ensamblado llamador no era dinámico, de eso estoy seguro. Así como que estoy seguro de que no había ensamblados dinámicos ni como referencia del ensamblado que lo llamaba, ni en la carpeta bin. Pero sí que los había en el dominio de aplicación (AppDomain), con lo cual, a la hora de localizar los recursos, estaba buscando en los ensamblados ya cargados en el AppDomain, y lo estaba haciendo antes de mirar en los ensamblados del directorio de trabajo, ya que si no, habría localizado los recursos sin problemas.

El hecho de que el número de excepciones por constructor fuera aumentando quedaba resuelto de manera sencilla. Según se fuesen generando más ensamblados dinámicos y se fuesen cargando en el AppDomain, se lanzaba una excepción por cada ensamblado dinámico en el AppDomain en cada invocación del constructor.

Vale, ya sé que generar ensamblados dinámicamente (Reflection.Emit) no es algo que se haga todos los días. Pero me ha hecho pensar un poco en cómo escribir las cadenas de conexión para que .NET vaya directamente al ensamblado que tiene incrustados los recursos, y no se de un garbeo cargando todos los assemblies del lugar para buscar un recurso.

Corolario: Cuando especifiques una cadena de conexión de Entity Framework, si está incrustada como recurso en un ensamblado (que es la opción por defecto), indica siempre el nombre del ensamblado donde está ese recurso. ¡Huye de los asteriscos, no suelen ser buenos compañeros! (y si no, se lo preguntas al que hizo un rm -rf * sobre /).

Metadata=res://<assemblyFullName>/<resourceName>

P.D.: Aunque en la documentación diga que tienes que poner el nombre completo del ensamblado, en realidad no es así. No es necesario especificar la versión, ni la cultura, ni la clave pública del ensamblado…

Si sigo así, la próxima entrada será en el 2012… Saludos a todos!