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!

No hay comentarios:

Publicar un comentario