martes, 20 de noviembre de 2012

Transformación de ficheros de configuración en ASP.NET

Aunque hace tiempo que conocía que se podía utilizar la transformación de los ficheros de configuración, es ahora cuando tengo una necesidad real de utilizar esta característica para intentar optimizar la publicación de aplicaciones web en entornos de hosting compartido.

Está claro que nuestro fichero web.config del entorno de desarrollo no es el mismo fichero web.config de producción o de cualquier otro entorno de publicación. Normalmente serán varias las secciones del web.config que tendremos que cambiar al publicar (appSettings, connectionStrings, customErrors, etc.). De igual forma está claro que no queremos (ni debemos) realizar estos cambios de forma manual cada vez que publiquemos en un entorno distinto al actual, por lo que una solución válida para automatizar esta tarea pasa por utilizar los ficheros de transformación.

Podemos crear un fichero de transformación para cada configuración de compilación (build configuration). Por defecto, cualquier proyecto incorpora las configuraciones Debug y Release, pero podemos crear cualquier otra configuración que necesitemos, por ejemplo: PreProducción, Producción, etc. Por cada configuración tendremos asociado a nuestro fichero web.config base un fichero con el nombre Web.NombreConfiguración.config. Cuando publiquemos y en función de la configuración activa, el fichero de transformación seleccionado aplicará los cambios solicitados al fichero web.config base y dará como resultado un nuevo fichero web.config con las transformaciones aplicadas.

Un ejemplo nos ayudará mejor a entenderlo.

Para crear una nueva configuración, Build > Configuration Manager… > New…

clip_image003

Después y en el menú contextual del fichero web.config, Add Config Tranform

clip_image004

Add Config Transform detectará que configuraciones de compilación no tienen un fichero de transformación y los agregará. Si estás utilizando VB.NET tendrás que activar “Show All Files” en la ventana “Solution Explorer” para ver estos ficheros.

clip_image005

Esta práctica no está limitada al directorio raíz de tu aplicación, sino que en cualquier carpeta que tenga un fichero web.config puedes aplicar la misma técnica.

clip_image006

También puedes borrar en cualquier momento un fichero de transformación y así y durante la publicación, simplemente no se realizará ninguna transformación.

En lo relativo a qué podemos hacer con las transformaciones, principalmente tendremos que trabajar con características principales:

  • Locator. Que nos ayudará a localizar el nodo donde aplicar una transformación.
  • Tranform. Servirá para indicar que tipo de transformación queremos realizar al nodo “localizado”.

La documentación sobre la sintaxis del espacio de nombres XML Document-Transform la puedes encontrar en la MSDN http://msdn.microsoft.com/en-us/library/dd465326(v=vs.100).aspx

Locator admite 3 posibilidades:

  • Condition(expresión XPath)
  • Match(Lista de atributos separada por comas)
  • XPath(expresión XPath)

Veamos algunos ejemplos:

Para empezar tenemos un fichero web.config con el siguiente contenido:

  <add

    name="CONEXIÓN"

    connectionString="CADENA_CONEXIÓN"

    providerName="PROVEEDOR" />

Con Locator="Condition(XPath expression)"

<?xml version="1.0" encoding="utf-8"?>

<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">

  <connectionStrings>

    <add

      name="NUEVO_NOMBRE"

      connectionString="NUEVA_CADENA_CONEXIÓN"

      providerName="NUEVO_PROVEEDOR"

      xdt:Locator="Condition(@name='CONEXION')"

      xdt:Transform="Replace"/>

  </connectionStrings>

</configuration>

Si quisiéramos ver ahora el resultado del fichero de transformación, la opción más sencilla sería publicar con la configuración “Debug”. Sin embargo, como a veces publicar puede ser algo tedioso (aunque sea en un directorio a disco), también podemos optar por llamar directamente por línea de comandos a MSBUILD con la siguiente sintaxis:
MSBuild “Ruta al fichero.vbproj|.csproj de nuestro proyecto” /t:TransformWebConfig /p:Configuration=NombreConfiguracion que nos volcará en el directorio \obj el resultado de la transformación (fijarse como además también ha trabajado con las subcarpetas).

clip_image007

Con Locator="Match(Lista de atributos separada por comas)"

  <connectionStrings>

    <add

      name="CONEXIÓN"

      connectionString="NUEVA_CADENA_CONEXIÓN"

      providerName="NUEVO_PROVEEDOR"

      xdt:Locator="Match(name)"

      xdt:Transform="Replace"/>

  </connectionStrings>

Si hablamos ahora de Transform, tenemos disponibles las siguientes operaciones:

  • Replace
    • Reemplaza el elemento encontrado.
    • En caso de encontrar varios, sólo reemplaza el primero.
  • Insert
    • Añade el elemento al final de la colección.
  • InsertBefore
    • Añade el elemento antes del elemento seleccionado según una expresión XPath.
  • InsertAfter
    • Igual que InsertBefore, pero añade el elemento después del elemento seleccionado.
  • Remove
    • Eliminar el elemento seleccionado.
    • En caso de encontrar varios, sólo reemplaza el primero.
  • RemoveAll
    • Elimina todos los elementos seleccionados.
  • RemoveAttributes
    • Elimina atributos del elemento seleccionado.
  • SetAttributes
    • Establece el valor de atributos de todos los elementos seleccionados.
    • Es similar a Replace, pero permite especificar que atributos serán reemplazados (mientras que Replace reemplaza el elemento entero). Además, SetAttributes no sólo reemplaza la primera ocurrencia, sino todas las encontradas.

Un ejemplo típico de un fichero de transformación para producción (release) podría ser aquel en que transformaremos los siguientes elementos:

  • appSettings
  • connectionStrings
  • compilation
  • customErrors
  • ELMAH
  • mailSettings

 

<?xml version="1.0" encoding="utf-8"?>

<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">

  <appSettings>

    <add

      key="Value"

      value="Value"

      xdt:Locator="Match(key)"

      xdt:Transform="Replace" />

  </appSettings>

  <connectionStrings xdt:Transform="Replace">

    <add

      name="Value"

      connectionString="Value"

      providerName="Value" />

  </connectionStrings>

  <system.web>

    <compilation xdt:Transform="RemoveAttributes(debug)" />

    <customErrors mode="RemoteOnly" xdt:Transform="SetAttributes(mode)">

    </customErrors>

  </system.web>

  <system.net>

    <mailSettings xdt:Transform="Replace">

      <smtp from=" Value ">

        <network

          host="Value"

          password="Value"

          userName="Value" />

      </smtp>

    </mailSettings>

  </system.net>

  <elmah>

    <security

      allowRemoteAccess="false"

      xdt:Transform="Replace" />

    <errorMail

      from="Value"

      to="Value"

      subject="Value"

      async="true"

      smtpPort="25"

      smtpServer="Value"

      userName="Value"

      password="Value"

      xdt:Transform="Replace">

    </errorMail>

  </elmah>

</configuration>

Las transformaciones realizadas han sido:

  • En appSettings hemos tenido que localizar primero con Match (porque asumimos que no queremos remplazar toda la sección sino sólo algún valor) y después se ha optado por Reemplazar (Replace). También se podría haber utilizado SetAttributes o incluso SetAttributes(value).
  • En connectionStrings se ha utilizado Replace pero sin ningún Locator. Con esto conseguimos remplazar la sección entera y no ha sido necesario utilizar Locator porque sólo hay una sección connectionStrings en un fichero web.config y por ello se localiza automáticamente.
  • En compilation se elimina el atributo debug con RemoveAttributes.
  • En customErrors simplemente se cambia el valor de mode con SetAttributes(mode).
  • Para mailSettings, elmah/security y elmah/errorMail se utilizar Replace sin Locator.

Espero que te haya quedado más o menos claro las posibilidades que ofrecen la transformación de ficheros de configuración y lo utilices de ahora en adelante, en detrimento del cambio manual que, insisto, ni nos gusta ni tampoco debemos.

POST ACTUALIZADO: Por cierto, si utilizas configSource para sacar a un fichero externo las cadenas de conexión o los settings de la aplicación, la transformación no los tendrá en cuenta, con lo cual tenemos varias opciones para solucionarlo. Una es utilizar distintos ficheros connectionStrings.config (p. ej. connectionStrings.Debug.config, connectionsString.Release.config y aplicar transformaciones al web.config para que apunte a uno u otro según configuración… cosa que no me gusta mucho, porque además de repetir código, no impide que en el directorio de publicación se escriban todos los ficheros, sean o no necesarios según la configuración activa). Otra es utilizar el addin de Visual Studio SlowCheetah que habilita la transformación de ficheros .config, no sólo al web.config sino a cualquier fichero de configuración.  Aunque la documentación de la herramienta es muy buena, también te dejo un enlace de un post de Scott Hanselman al respecto.

El único problema que me ha dado SlowCheetah ha sido un error durante la publicación del proyecto web porque no encontraba ciertos ficheros. Para solucionarlo he tenido que copiar todos los ficheros de la carpeta C:\Users\<TuUsuario>\AppData\Local\Microsoft\MSBuild\SlowCheetah\v1 a la carpeta v2.5.10. Entiendo que esto lo solucionará el autor en un siguiente release, seguro que sí!

POST ACTUALIZADO: Una mejor solución que copiar los ficheros parece modificar el fichero .csproj e indicar allí la ruta correcta a los ficheros:

<PropertyGroup>
  <SlowCheetahTargets Condition=" '$(SlowCheetahTargets)'=='' ">$(LOCALAPPDATA)\Microsoft\MSBuild\SlowCheetah\v2.5.10\SlowCheetah.Transforms.targets</SlowCheetahTargets>
</PropertyGroup>

POST ACTUALIZADO: Parece que hay un problema si se quiere reemplazar el nodo entero. Por ejemplo, algo como lo siguiente falla:

<connectionStrings xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform" xdt:Transform="Replace">

</connectionStrings>

Lanzado el siguiente error:

Could not write Destination file: Cannot insert the node in the specified location

Siendo así, en el caso de connectionStrings optaremos por tener distintos ficheros según configuración en vez de aplicar transformaciones. Donde sí tenemos que aplicar una transformación es en los ficheros web.config para que apunten a uno u otro fichero connectionString desde la propiedad configSource.

<connectionStrings configSource="connectionStrings.Release.config" xdt:Transform="Replace"></connectionStrings>

Esto conlleva por otro lado, que al publicar (en mi caso lo hago a disco) tengamos tanto el fichero connectionStrings.config (el que utilizamos en debug) como el fichero connectionStrings.Release.config (el que hemos creado para release). Como no queremos (no debemos) tener que eliminar a mano el fichero connectionStrings.config después de publicar, podemos solucionar esto incluyendo algo de código MSBuild en el fichero .pubxml que se creó en el directorio PublishProfiles debajo de Properties en nuestra aplicación web.

El código a incluir es el siguiente… y ojito que este código he tardado en escribirlo 3 horas, así que utilízalo sabiamente ;-)

<Target Name="panicoenlaxbox" AfterTargets="GatherAllFilesToPublish">
  <Delete Files="$(_PackageTempDir)\connectionStrings.config" />   
</Target>

POST ACTUALIZADO: Al volver mucho después a este post he visto que ahora también hay un paquete de Nuget SlowCheetah. Esto significa que ya no es necesario instalar el plugin en VS. Personalmente me parece ahora mejor opción porque así te olvidas de tener que estar copiando manualmente y en una ruta concreta los ficheros .targets en el servidor de integración continua, simplemente ahora son parte del proyecto.

image

Un saludo!

lunes, 19 de noviembre de 2012

Una y no más, Santo Crystal Reports

Tristemente, en mi empresa tenemos alguna aplicación que utiliza Crystal Reports. Más tristemente, la versión de Crystal Reports es 2008. Digo esto último porque quizás futuras versiones faciliten más la vida al desarrollador, porque desde luego la versión 2008 no es nada agradecida.

A modo de diario y previendo futuras publicaciones de esta misma aplicación, dejo a continuación los pasos que hemos tenido que llevar a cabo para hacer que los informes de Crystal funcionen en ASP.NET.

El sistema operativo de despliegue es un Windows Web Server 2008 R2. La versión del framework de la aplicación de ASP.NET es 3.5… y lo dicho, la versión de Crystal es 2008.

Lo primero es descargar el runtime de CR. Podría parecer sencillo, pero no lo es. Los señores de SAP han decidido que sea todo un galimatías encontrar algo en su página, así que si no tiene el enlace directo podrías vértelas canutas para descargar el runtime, pero bueno…

La página de descarga de diversas utilidades (entre ellas el runtime) es https://websmp230.sap-ag.de/sap%28bD1lcyZjPTAwMQ==%29/bc/bsp/spn/bobj_download/main.htm Aquí tienes que localizar “Crystal Reports 2008 Service Pack 4 – Redist Install”. https://smpdl.sap-ag.de/~sapidp/012002523100008782532011E/cr2008sp4_redist.zip Me hubiera gustado bajarme el SP5 que se anuncia en https://wiki.sdn.sap.com/wiki/pages/viewpage.action?pageId=56787567 pero me ha sido imposible registrarme y la descarga pide nombre de usuario y contraseña.

Una vez descargado, se instala y listo. Crea una aplicación web llamada “crystalreportviewers12” en el sitio web por defecto (tampoco pregunta donde quieres crearla no vaya a ser que sea de utilidad, que aquí estamos para complicar los cosas… habrá pensado quién hizo el paquete de instalación). Como nuestra aplicación no está en el sitio web por defecto (está en otro sitio web), pues toca crear un directorio virtual en nuestro sitio web de la aplicación, apuntando a C:\Program Files (x86)\Business Objects\Common\4.0\crystalreportviewers12 con el nombre “crystalreportviewers12”. Además, también he tenido que editar el fichero web.config de la anterior ruta y quitar la siguiente sección

<system.web>

<!-- request length is 20MB -->

<httpRuntime maxRequestLength="20000"/>

<sessionState timeout="20" />

</system.web>

Como parte de la instalación también ha agregado al sitio web por defecto, una carpeta aspnet_client. No preguntes y cópiala también en la raíz de tu sitio web.

Otra cosa un tanto hiriente es que tendrás que compilar tu aplicación en x86. En el siguiente foro lo explican bien http://scn.sap.com/thread/1519581, pero básicamente “Only the .NET 2005 (CR 10.2) and .NET 2008 (CR 10.5) bundles are 64 bit. You must compile the app as 32 bit and use the 32 bit CR runtime.”

Además, también tendrás que habilitar en el grupo de aplicaciones en que esté asignada tu aplicación “Habilitar aplicaciones de 32 bits”.

clip_image001

Aunque en esta publicación no me ha pasado, recuerdo también haber encontrado problemas con la típica imagen de logo del cliente en un informe. En su día me ayudaron los siguientes enlaces (problemas con permisos, problemas con CrystalImageHandler.aspx, etc.) http://www.sdn.sap.com/irj/boc/go/portal/prtroot/docs/library/uuid/a0437ea8-97d2-2b10-2795-c202a76a5e80?QuickLink=index&overridelayout=true

También es posible que tengas que mapear CrystalImageHandler.aspx en el IIS. Visita este enlace para más información http://crmtogether.com/mwCrystal/index.php?title=Troubleshoot

La moraleja de todo esto es que “una no más, santo tomás”. No volveré a usar Crystal Reports para ninguna tarea de reporting. Probaré con otros soluciones de terceros o incluso me planteo hacerme los informes a mano. De hecho, creo que los informes, cada vez más, son un recurso en desuso que solo sirven a un pocos y durante un tiempo limitado (después los requerimientos cambian y nos quedamos con una ristra de informes que no valen para nada).

Un saludo!