Editando las connection strings «en caliente» de un proyecto ASP.net

Un problema peculiar con el que me he encontrado recientemente tiene que ver con dos equipos con la misma base de código y un punto de sincronización común, que en este caso es o bien dos bases de datos distingas del mismo servidor, o a la misma base de datos con diferentes credenciales, en un gráfico como el que se muestra.

Capture

Para guardar la configuración de la base de datos recurrimos habitualmente al apartado connectionStrings de nuestro fichero web.config. Dicho fichero cuenta además con diferentes transformaciones según la configuración de build seleccionada, como muestra la siguiente imagen:

Capture_debug

Pero… las transformaciones solamente se ejecutan al desplegar un proyecto…

El hecho de no poder ejecutar las transformaciones en tiempo de compilación no nos permite depurar nuestra solución con las cadenas de conexión necesarias, y, aunque existe una manera de transformar el fichero web.config durante la compilación, quería buscar una aproximación diferente.

Editando el objeto ConnectionStrings

Dentro del código de nuestra aplicación disponemos del objeto ConfigurationManager.ConnectionStrings que nos da una colección de solo lectura en la que podemos acceder a los datos. Si intentamos escribir en dicha colección obtenemos la siguiente excepción:

An exception of type 'System.Configuration.ConfigurationErrorsException' occurred in System.Configuration.dll but was not handled in user code

Additional information: The configuration is read only.

Sin embargo si vamos al código fuente de ConnectionStrings veremos que el atributo connectionString tiene setter, con lo cual en teoría podemos fijar el valor:

public string ConnectionString {
    get {
        return (string)base[_propConnectionString];
    }
    set {
        base[_propConnectionString] = value;
    }
}

Si vamos un poco más abajo nos encontraremos con la clase ConfigurationElement, cuyo método IsReadOnly() se comprueba al intentar fijar un valor.

protected void SetPropertyValue(ConfigurationProperty prop, object value, bool ignoreLocks) {
if (IsReadOnly()) {
    throw new ConfigurationErrorsException(SR.GetString(SR.Config_base_read_only));
}

public virtual bool IsReadOnly() {
    return _bReadOnly;
}

Este valor IsReadOnly lee una variable _bReadOnly, y no sería maravilloso que pudiéramos editar esa variable y editar nuestras cadenas de conexión? Pues gracias a Reflection podemos hacerlo!:

ConnectionStringSettings connectionString = ConfigurationManager.ConnectionStrings[0];
var field = typeof(ConfigurationElement).GetField("_bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);
field.SetValue(connectionString, false);
connectionString.ConnectionString = "CustomConnectionString";
field.SetValue(connectionString, true);

Tras nuestra edición podemos volver a establecer el atributo _bReadOnly a true restaurando el bloqueo de la escritura. Hemos de tener en cuenta que este proceso ha de realizarse lo antes posible en nuestra aplicación, así que yo usaría Startup.cs o Global.asax.cs (recordemos que este último desaparece con ASP.net vNext).

Cargando nuestras connection strings personalizadas

Una vez que sabemos que podemos cargar las connection strings en caliente, el siguiente paso es cargar el fichero que contiene las transformaciones:

string path = HostingEnvironment.MapPath("~/Web.TEAM1.config");
XmlDocument doc = new XmlDocument();
doc.Load(path);
XmlNodeList list = doc.DocumentElement.SelectNodes(string.Format("connectionStrings/add[@name='{0}']", "Title"));
XmlNode node = list[0];
string connectionStringToReplace = node.Attributes["connectionString"].Value;

En este caso cargamos el fichero de TEAM1, buscamos la cadena de conexión con la clave especificada y la asignamos a la variable connectionStringToReplace.

Código completo

private void ReplaceConnectionStrings(string team)
{
    string path = HostingEnvironment.MapPath(string.Format("~/Web.{0}.config", team));
    XmlDocument doc = new XmlDocument();
    doc.Load(path);

    foreach (ConnectionStringSettings connectionString in ConfigurationManager.ConnectionStrings)
    {
        XmlNodeList list = doc.DocumentElement.SelectNodes(string.Format("connectionStrings/add[@name='{0}']", connectionString.Name));
        if (list.Count == 0) continue;

        XmlNode node = list[0];

        var field = typeof(ConfigurationElement).GetField("_bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);
        string connectionStringToReplace = node.Attributes["connectionString"].Value;

        field.SetValue(connectionString, false);
        connectionString.ConnectionString = connectionStringToReplace;
        field.SetValue(connectionString, true);
    }
}

Este código recorre todas las Connection Strings, y si encuentra una en nuestro fichero de transformaciones, la reemplaza.

Finalmente, directivas de preprocesador

Las configuraciones de Build nos permiten fijar ciertas directivas a nuestros proyectos, y estas directivas se aplicarán en tiempo de compilación. De esta manera si alguna de las dos claves está presente se sustituirán, en caso de que ninguna esté presente no se realizará una sustitución.

#if TEAM1
    ReplaceConnectionStrings("TEAM1");
#elif TEAM2
    ReplaceConnectionStrings("TEAM2");
#endif

A continuación se pueden ver dos ejemplos para los diferentes equipos.

Capture_team1

Capture_team2

Conclusiones

Esta solución nos da una manera efectiva de poder depurar nuestro código sin afectar el ciclo de vida del fichero web.config, sus transformaciones, y el comportamiento por defecto. El código agregado solamente se ejecutaría en caso de que alguna de las dos directivas se cumpliese, con lo cual podemos tener diferentes configuraciones para diferentes equipos.

Reflection es una característica que nos permite sacar mucho más de lo que un objeto nos ofrece públicamente, a costa de un impacto en rendimiento y requerir un mayor conocimiento de lo que estamos haciendo, de ahí a que se use con moderación.

Enlaces adicionales