update-database y LocalDb en una aplicación de escritorio

Estos días he estado desarrollando un aplicación de escritorio (wpf aunque eso es lo de menos) que va a hacer uso de LocalDb para guardar datos. Ciertamente no es un escenario muy habitual, ya que al instalar la aplicación en un ordenador cliente se requiere instalar LocalDb pero en este caso eso era asumible. Otras opciones para escritorio podrían pasar por usar algúna BBDD de proceso (como VistaDb o similares).

“Teoricamente” eso no debería diferir del workflow usado en aplicaciones web. Supongamos que tenemos nuestro proyecto con el contexto de EF creado y hemos habilitado migrations (enable-migrations) para controlar las modificaciones del esquema.

Supongamos una cadena de conexión que use AttachDbFilename:

  1. <add name="DbClient" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\App_Data\clientdb_v1.mdf;Initial Catalog=clientdb-v1;Integrated Security=True" providerName="System.Data.SqlClient" />

Se puede observar que se usa |DataDirectory| al igual que en una aplicación web. Esta es una variable de “entorno” que entiende el .NET Framework (a partir de la 4.0.2) y cuyos valores están definidos de la siguiente manera, según se cuenta en este post.

By default, the |DataDirectory| variable will be expanded as follow:

– For applications placed in a directory on the user machine, this will be the app’s (.exe) folder.
– For apps running under ClickOnce, this will be a special data folder created by ClickOnce
– For Web apps, this will be the App_Data folder

Under the hood, the value for |DataDirectory| simply comes from a property on the app domain. It is possible to change that value and override the default behavior by doing this:

      AppDomain.CurrentDomain.SetData("DataDirectory", newpath)

Por lo tanto tenemos que para aplicaciones de escritorio |DataDirectory| se mapea al directorio donde está la BBDD según este post. He de decir que en mi experiencia eso NO es cierto. Se mapea a la subcarpeta App_Data de la carpeta donde está el ejecutable (así, si tenemos el ejecutable en c:xxxbinDebug se mapeará a c:xxxbinDebugApp_Data). Quizá es un cambio posterior a la publicación de este post.

En ejecución se espera que la BBDD (el fichero .mdf) esté en este directorio. Perfecto, pero ahora tenemos el problema de las herramientas de VS. Antes de nada, agregamos un archivo .mdf a nuestra solución (en el startup project):

image

Con esto ya podemos ejecutar update-database y transferir todas las migraciones a este fichero .mdf.

Pero ahora tenemos un problema, y es que los ficheros .mdf son tratados como un fichero “content” tradicional, es decir que lo máximo que VS puede hacer es copiarlos al output folder:

image

Si marcamos:

  1. Copy always: Cada vez que compilemos el proyecto se copiará el .mdf hacia el directorio de ejecución. Resultado: perderemos todos los datos, dado que el .mdf de la solución está vacío (solo tiene el esquema generado por update-database).
  2. Copy if newer: Solo se copiará en caso que el .mdf de la solución sea más nuevo, lo que solo ocurrirá en el caso de cambios de esquema. Entonces en cada cambio de esquema perdemos los datos.
  3. Do not copy: El fichero .mdf no se copia en el directorio de salida, lo que implica que la aplicación… no lo encontrará.

Ninguna de las 3 opciones es deseable. Esto en aplicaciones web no ocurre, debido a la forma en como DataDirectory es gestionado por ASP.NET, pero ahora estamos en escritorio 🙁

Una posible solución es olvidarnos de la copia (Do not copy) y hacer que DataDirectory apunte al fichero .mdf de la solución. Para ello se puede usar AppDomain.CurrentDomain.SetData para conseguir el efecto deseado:

  1. var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + @"\..\..\MyClient\App_Data";
  2. AppDomain.CurrentDomain.SetData("DataDirectory", path);

Básicamente obtenemos el directorio de ejecución del assembly y nos movemos hacia el directorio donde hay realmente el fichero en la solución.

Eso hace que ahora tanto VS al usar update-database como nuestra aplicación usen el mismo fichero .mdf. Por supuesto en cuanto despleguemos de verdad la app deberemos utilizar otra técnica. Porque así dependemos de un fichero (el .mdf) que NO está desplegado en el directorio de salida.

Una solución para ello es volver a poner el “Copy aways” (por lo que el .mdf se copiará cada vez, lo que ahora  no es problema porque tiene datos y esquema) y ejecutar o no la llamada a AppDomain.CurrentDomain.SetData según sea de menester (si se ejecuta la llamada se usa el .mdf de la solución, en caso contrario se usa el .mdf localizado en el directorio de salida).

Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *