Cómo detectar registros duplicados en una base de datos

Post original en JASoft.org: http://www.jasoft.org/Blog/post/Como-detectar-registros-duplicados-en-una-base-de-datos.aspx

CopiaEsta es una consulta muy sencilla pero que a muchos principiantes les resulta de mucha utilidad, así que como hoy he tenido que usarla y me he acordado, la pongo aquí por si a alguien le viene bien.

El problema es el habitual: tenemos una tabla con muchos registros y queremos saber si alguno de ellos está duplicado. Lo más común no es que nos interese realmente saber qué registros están duplicados, sino qué registros tienen dos o tres campos iguales.

Por ejemplo, si tenemos una lista de clientes quizá no nos interesa saber quiénes están duplicados buscándolos por el nombre (ya que éste puede haber sido escrito diferente: con o sin tilde, con abreviatura o no), por lo que resultaría más interesante averiguar aquellos registros que tienen el teléfono y el email duplicados por ejemplo.

La consulta es muy fácil, y usa una función de agrupamiento para determinarlo. La consulta genérica sería esta:

   1: SELECT COUNT(*), CAMPO_A, CAMPO_B

   2: FROM TABLA

   3: GROUP BY CAMPO_A, CAMPO_B

   4: HAVING COUNT(*)>1;

Así pues, para el ejemplo de los clientes duplicados escribiríamos algo como esto:

   1: SELECT Telefono, EMail, COUNT(*)

   2: FROM Clientes

   3: GROUP BY Telefono, EMail

   4: HAVING COUNT(*)>1;

Y nos devolvería una lista de los clientes cuyo email y teléfono coinciden exactamente, y cuántas veces (para saber cuántas veces los tenemos repetidos).

A partir de aquí ya es más o menos fácil (según la estructura de nuestra tabla y el número de campos involucrados) sacar un listado completo usando lo anterior en sub-consultas.

Este tipo de consulta funcionará sin problemas en cualquier gestor de bases de datos relacionales (SQL Server, MySQL, Oracle,… incluso Access)

Sencillo pero útil.

¡Espero que te sirva de ayuda!

Bloquear los botones mientras se envía un formulario

Post original en JASoft.org: http://www.jasoft.org/Blog/post/Bloquear-los-botones-mientras-se-envia-un-formulario.aspx

Un efecto bastante indeseable en una página web es que los usuarios puedan enviar al servidor dos o más veces una misma información.

Esto suele ocurrir porque existe una latencia entre el cliente y el servidor que hace que, si el servidor es lento o si hay mucha información que enviar o recibir en el proceso, la página tarde varios segundos en desaparecer del navegador.  Por lo tanto un usuario impaciente puede pulsar varias veces el botón de envío, recibiéndose varias veces la información en el servidor.

Esto, según sea el sistema que hemos diseñado, puede tener resultados desastrosos para nuestra aplicación.

Evitarlo par aun botón concreto es muy sencillo. Basta con deshabilitarlo usando un código como este:

   1: <input type="submit" value="Enviar!" onclick="this.disabled=true;">

De esta forma cuando se pulse el botón, aparte de efectuar su función habitual de enviar el formulario (es un botón de tipo “submit”) conseguiremos que éste se deshabilite y por lo tanto que el usuario no pueda pulsarlo más que la primera vez.

El aspecto del botón deshabilitado es como este:

Submit_Disabled

Fíjate en que el botón aparece plano y desdibujado, y no es posible pulsarlo ni interactuar con él.

Hacerlo automáticamente en cualquier página y cualquier botón

Lo anterior es muy sencillo y rápido de conseguir, pero ¿no sería estupendo poder conseguir lo mismo de manera automática en todos los formularios que haya en una página y sin necesidad de ir botón por botón?

Además existen otros problemas asociados con lo anterior. Por ejemplo, si queremos hacer más cosas antes de enviar el formulario (por ejemplo validarlo) y no siempre se va a enviar al servidor por el mero hecho de pulsar el botón, entonces tendremos que complicar el código un poco más y no sería tan directo. Por otro lado los formularios se pueden enviar también solo con pulsar la tecla ENTER al estar dentro de cualquier campo de texto, en cuyo caso nuestro botón no se enteraría y podría ser pulsado también.

He escrito un pequeño código contenido en un archivo .js que usa otra técnica para interceptar el envío del los formularios y en caso de enviar desactiva el botón submit correspondiente. Esto lo hace de manera automática con tan solo incluirlo en cualquier página. Su contenido es el siguiente:

   1: var tmrReady = setInterval(isPageFullyLoaded, 100);

   2:  

   3: function isPageFullyLoaded() {

   4:     if (document.readyState == "loaded" || document.readyState == "complete") {

   5:         subclassForms();

   6:         clearInterval(tmrReady);

   7:     }

   8: }

   9:  

  10: function submitDisabled(_form, currSubmit) {

  11:     return function () {

  12:         var mustSubmit = true;

  13:         if (currSubmit != null)

  14:             mustSubmit = currSubmit();

  15:  

  16:         var els = _form.elements;

  17:         for (var i = 0; i < els.length; i++) {

  18:             if (els[i].type == "submit")

  19:                 if (mustSubmit)

  20:                     els[i].disabled = true;

  21:         }

  22:         return mustSubmit;

  23:     }

  24: }

  25:  

  26: function subclassForms() {

  27:     for (var f = 0; f < document.forms.length; f++) {

  28:         var frm = document.forms[f];

  29:         frm.onsubmit = submitDisabled(frm, frm.onsubmit);

  30:     }

  31: }

La primera parte del código (hasta la línea 8) se encarga de llamar al método subclassForms una vez que la página esté lista para ser manipulada desde JavaScript. Puedes ver cómo funciona esta técnica en mi anterior post.

Lo interesante aquí es que sub-clasificamos el evento onsubmit de los formularios de nuestra página de modo que le añadimos código propio. La sub-clasificación de un método o un evento consiste en sustituir el código original (de haberlo) por uno propio pero al mismo tiempo conservando toda la funcionalidad inicial.

Lo que hace el método subclassForms es recorrer todos los formularios que haya en la página e interceptar su evento onsubmit para que cuando se envíe el formulario al servidor se ejecute nuestro código. Para ello definimos una función submitDiabled que es una clausura o closure. Éstas nos permiten crear funciones genéricas pero que conservan parámetros internos para una ejecución posterior (ver mi post al respecto).

Como parámetros para la función le pasamos el formulario actual y el código del evento onsubmit de éste (que puede que tenga o no). El objeto de este segundo parámetro es conservar toda la funcionalidad del método onsubmit existente aunque nosotros vayamos a hacer más cosas. Es la sub-clasificación de la que hablaba antes.

Si vemos el código de la clausura, nuestra función lo que hace es, antes de nada suponer que el formulario se va a enviar (que es la acción por defecto), por eso se define una variable mustSubmit que por defecto vale true.

Si originalmente existía un código específico para ejecutar en el evento de envío del formulario (línea 13) entonces lo ejecutamos llamándolo con los paréntesis, y guardamos el resultado en la variable mustSubmit anterior. Como nota de cultura general diré que si el resultado de un evento onsubmit es true es que debe enviarse el formulario. Si por el contrario devuelve un false entonces el formulario no se envía.

Bien. Ahora recorremos todos los elementos del formulario en cuestión y verificamos si hay alguno que sea del tipo “submit”, o sea, un botón de envío de formulario. En caso de encontrarlo, si realmente se va a enviar el formulario (es decir, si mustSubmit es true, línea 19) entonces deshabilitamos el botón para evitar que pueda pulsarse más de una vez.

Finalmente (línea 22) se devuelve el resultado apropiado para enviar o no el formulario.

Con este código conseguimos el desactivado de los botones evitando los problemas que mencionábamos antes y además de forma completamente transparente ya que no se ve afectada la funcionalidad del evento onsubmit.

Podríamos incluso ir un paso más allá y hacerlo más seguro detectando si se ha enviado el formulario ya o no, evitando así un doble envío directamente en el código de nuestro evento onsubmit sub-clasificado (devolviendo siempre false si el evento se llama más de una vez). Pero con esto debería ser suficiente para la mayor parte de los casos.

El archivo con este código lo puedes descargar desde aquí: disableSubmits.zip (ZIP, 514 bytes). Para usar la funcionalidad lo único que tienes que hacer es copiar el .js a la misma carpeta que tu página e incluir una línea como esta en la cabecera de la misma:

   1: <script language="javascript" type="text/javascript" src="disableSubmits.js"></script>

¡Espero que te resulte útil!

Detectar que la página actual está lista

Post original en JASoft.org: http://www.jasoft.org/Blog/post/Detectar-que-la-pagina-actual-esta-lista.aspx

Esto a simple vista es una tarea sencilla, ya que de toda la vida las páginas HTML disponen de un evento onload que nos permite detectar cuando el documento ha cargado por completo.

Por ejemplo, si escribimos:

   1: <body onload="doWhatYouNeed();">

   2: ......

   3: </body>

spongebob_readyEn cuanto el documento esté completamente cargado se llamará de manera automática a la función doWhatYouNeed.

Nota: A partir de ahora haré todos los ejemplos prácticos en inglés para facilitar su localización posterior a este idioma, así que no te extrañes de ver los nombres de las variables, métodos… e incluso la interfaz de usuario en el idioma de Shakespeare.

Esto nos asegura que cuando se llame a la función todos los elementos de la página estarán cargados y listos para su uso, incluyendo los que hay que cargar en una segunda fase tras la carga del HTML de la página: imágenes, CSS, etc…

Sin embargo en muchas ocasiones nos hará falta detectar una fase anterior en el ciclo de vida de la página: cuando la jerarquía de objetos está lista para ser utilizada. Esto implica que se ha interpretado todo el HTML, se han cargado los scripts y por lo tanto se puede manipular el contenido del documento sin problemas. En este momento puede que todavía no se hayan cargado las imágenes y otros elementos, pero podemos manipular sin problemas el DOM ya que éste ha sido generado por completo.

jQuery nos ofrece el método ready() para detectar esta circunstancia, facilitándonos mucho la vida, pues basta con definir una función dentro de éste para que sea llamada en cuanto la página esté lista para ser manipulada.

En este post explicaré como conseguir esta misma funcionalidad directamente con JavaScript y para todos los navegadores del mercado, sin utilizar ninguna biblioteca externa.

La propiedad readyState

Esta propiedad readyState, del objeto document, disponible en todos los navegadores actuales (en Firefox desde la versión 3.6), nos devuelve una cadena de texto que indica el estado actual de la página. Los valores que puede tener son los siguientes, ordenados por el momento en el que se producen dentro del ciclo de vida de la página:

uninitialized No se ha inicializado aún
loading Se están empezando a cargar los datos de la página, e interpretando el HTML
loaded Se han cargado ya los elementos en la jerarquía de la página
interactive El usuario ya puede interactuar con los elementos de la página aunque todavía no estén cargados todos (por ejemplo, faltan las imágenes).
complete Se ha terminado de cargar la página completamente

De acuerdo. Entonces a partir de esta propiedad es sencillo determinar en qué estado se encuentra nuestra página, y si está en estado loaded o superior entonces sabemos que podemos ya interactuar con el DOM desde nuestros scripts.

Pero ¿cómo detectamos el cambio de estado?

Detectar el cambio de estado

Internet Explorer y Ópera ofrecen un evento para la página muy útil llamado onreadystatechange. Es posible interceptarlo y cada vez que cambie el estado se nos notificará de ello, pudiendo actuar en consecuencia. El problema que tiene su uso es que no es multiplataforma, ya que no funcionará en Firefox, ni Chrome ni Safari, ni en los navegadores móviles.

Entonces ¿cómo podemos hacer para solventar este problema?

Vamos a tomar otro camino y lo que haremos será comprobar en intervalos muy cortos el estado actual de la página usando un temporizador de JavaScript.

El código es muy sencillo:

   1: var tmrReady = setInterval(isPageFullyLoaded, 100);

   2:  

   3: function isPageFullyLoaded() {

   4:     if (document.readyState == "loaded" || document.readyState == "interactive" || document.readyState == "complete") {

   5:         doWhatYouWant();

   6:         clearInterval(tmrReady);

   7:     }

   8: }

Como vemos lo que se hace es crear un temporizador periódico, que cada 100 milisegundos llame a la función isPageFullyLoaded. En ésta lo que se hace es comprobar si el documento se encuentra actualmente en alguna de las fases en las que ya es posible manipular el DOM, en cuyo caso llamamos a la función que nos interese, para hacer lo que sea, y además (y esto es muy importante), eliminamos el temporizador pues ya no nos va a resultar útil.

Podríamos usar un intervalo menor (por ejemplo de 50 milisegundos) pero normalmente con 100 es más que suficiente y no así cargamos menos la ejecución inicial de la página.

Con este truco podremos detectar el estado de la página de forma muy sencilla y conseguir lo mismo que el ready de jQuery sin necesidad de cargar toda la biblioteca jQuery para algo tan pequeño.

¡Espero que te resulte útil!

Proveedores universales de ASP.NET: Membership y Roles fuera de SQL Server

Post original en JASoft.org: http://www.jasoft.org/Blog/post/Proveedores-universales-de-ASPNET-Membership-y-Roles-fuera-de-SQL-Server.aspx (siempre antes ahí)

Como todo programador de ASP.NET sabe, la mayor parte de las API de esta plataforma de desarrollo web están basadas en el modelo de proveedores. Para explicarlo de manera rápida, básicamente esto significa que entre la funcionalidad que el programador utiliza y el almacenamiento relacionado con ésta, existe un elemento intermedio llamado "proveedor" que desacopla ambas partes. De esta manera, si queremos utilizar otro tipo de almacenamiento basta con cambiar el proveedor en la configuración y listo. no es necesario tocar el código en absoluto, por lo que resulta muy cómodo y útil.

El siguiente esquema (sacado de MSDN) ilustra la arquitectura de este modelo:

ModeloProveedores

Como vemos muchos servicios como los de seguridad (Membership y Roles), la personalización (Profile) u otros como el almacenamiento de la sesión o los WebParts, están basados en este modelo, representados por las cajas de la parte superior. Así, la información de los usuarios, los roles, los perfiles, las sesiones, etc…. se pueden almacenar en cualquier ubicación para la que haya un proveedor apropiado: SQL Server, archivos XML, archivos de texto, al memoria del servidor… Lo que une una de estas API de la parte superior con el almacenamiento de la parte inferior son los proveedores, que aparecen en la figura en la franja intermedia. Así, por ejemplo, el proveedor SqlMembershipProvider sirve para almacenar la información de los usuarios en una base de datos SQL Server mientras que el ActiveDirectoryMembershipProvider utiliza el directorio activo como almacenamiento para los usuarios.

Los proveedores más utilizados son los de Sql Server, que nos permiten almacenar los datos en una base de datos de SQL Server 2005 o superior. Es posible escribir proveedores para otros sistemas de gestión de bases de datos relacionales como MySQL por ejemplo, pero básicamente estamos solos para utilizarlos pues no tienen soporte por parte de Microsoft.

Proveedores "universales"

Últimamente hemos visto como se está popularizando el uso de otros sistemas de almacenamiento dentro de la esfera de Microsoft. En concreto uno que toma cada vez más fuerza es SQL Server Azure,la versión en la nube del gestor de datos de Microsoft, que nos permitiría tener accesible para múltiples aplicaciones los datos de nuestros proveedores (por ejemplo una base de datos común de usuarios y roles).

Otro que tiene mucho interés es SQL Server Compact 4.0, la base de datos embebida, compatible con SQL Server, y que viene incluida con Visual Studio 2010. Es una opción ideal para desplegar en un hosting en el caso de aplicaciones que no tengan miles de usuarios ya que toda la información se despliega dentro de un único archivo con extensión .sdf, no necesita licencias ni configuración, no hay que instalar un gestor de datos y además es compatible con SQL Server en cuanto a tipos de datos y consultas, por lo que migrar más tarde a su hermano mayor es "pan comido".

Por defecto ASP.NET no ofrece soporte para ninguno de estos dos gestores de datos. Sin embargo en Junio de 2011 Microsoft liberó una versión alfa de los "ASP.NET Universal Providers". La versión 1.0 real apareció el 19 de agosto, y el mismo día liberaron una versión 1.0.1 que corregía algunos pequeños errores. Estos proveedores universales son una implementación 100% compatible con los proveedores nativos pero que nos permite elegir de manera muy sencilla qué gestor de datos queremos usar para el almacenamiento: SQL Server 2005 o superior (como los nativos), SQL Server Azure o SQL Server Compact.

Obviamente eso de "universal" es un poco exagerado ya que sólo dan soporte para estos tres gestores, pero imagino que Microsoft pretende con ellos dar soporte en el futuro a otros sistemas de almacenamiento. Además sólo implementan las APIs de Membership,Roles, Profile y Session, así que hay que tenerlo en cuenta.

Su principal ventaja es que, en lugar de cambiar el proveedor en la configuración de la aplicación, lo único que necesitas hacer para cambiar el almacén de datos es modificar la cadena de conexión. Al cambiar la cadena de conexión, automáticamente se cambia el almacén de datos. Así, trabajar contra la versión completa de SQL Server, la versión Azure o la Compact es un mero cambio de parámetros de conexión.

Vamos a verlo con un ejemplo.

Añadir los proveedores universales a un proyecto

Para añadir estos proveedores a nuestro proyecto debemos usar el gestor de paquetes integrado en Visual Studio, NuGet. Podemos hacerlo abriendo la línea de comandos de Nuget así:

ProveedoresUniversales_1
Pulsa para aumentar

Y luego escribiendo el comando resaltado en la siguiente figura desde la consola de NuGet:

ProveedoresUniversales_2
Pulsa para aumentar

Si preferimos una interfaz más visual podemos abrir el gestor de paquetes de NuGet (el segundo elemento del menú anterior), y buscar los proveedores universales con el buscador:

ProveedoresUniversales_3
Pulsa para aumentar

Tras instalarlos, sea un método u otro, obtenemos una referencia al ensamblado System.Web.Providers en nuestro proyecto:

ProveedoresUniversales_4

Este ensamblado tiene las clases nuevas que usaremos para sustituir a las nativas. La siguiente tabla muestra la equivalencia entre los proveedores "nativos" y los "universales":

API Proveedor "nativo" Proveedor "universal"
Membership System.Web.Security.SqlMembershipProvider System.Web.Providers.DefaultMembershipProvider
Roles System.Web.Security.SqlRoleProvider System.Web.Providers.DefaultRoleProvider
Profile System.Web.Profile.SqlProfileProvider System.Web.Providers.DefaultProfileProvider
Session Gestionado internamente por ASP.NET System.Web.Providers.DefaultSessionStateProvider

Además la instalación de paquete NuGet introduce unas líneas de configuración nuevas en nuestro web.config para definir los nuevos proveedores:

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

<configuration>

  <connectionStrings>

    <add name="ApplicationServices" connectionString="data source=.SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true" providerName="System.Data.SqlClient" />

    <add name="DefaultConnection" connectionString="Data Source=.SQLEXPRESS;AttachDbFilename=|DataDirectory|aspnet.mdf;Integrated Security=True;User Instance=True;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" />

  </connectionStrings>

  <system.web>

    <authentication mode="Forms">

      <forms loginUrl="~/Account/Login.aspx" timeout="2880" />

    </authentication>

    <membership defaultProvider="DefaultMembershipProvider">

      <providers>

        <clear />

        <add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="ApplicationServices" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" />

        <add name="DefaultMembershipProvider" type="System.Web.Providers.DefaultMembershipProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" />

      </providers>

    </membership>

    <profile defaultProvider="DefaultProfileProvider">

      <providers>

        <clear />

        <add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="ApplicationServices" applicationName="/" />

        <add name="DefaultProfileProvider" type="System.Web.Providers.DefaultProfileProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" applicationName="/" />

      </providers>

    </profile>

    <roleManager enabled="true" defaultProvider="DefaultRoleProvider">

      <providers>

        <clear />

        <add connectionStringName="ApplicationServices" applicationName="/"

          name="AspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider" />

        <add applicationName="/" name="AspNetWindowsTokenRoleProvider"

          type="System.Web.Security.WindowsTokenRoleProvider" />

        <add connectionStringName="DefaultConnection" applicationName="/"

          name="DefaultRoleProvider" type="System.Web.Providers.DefaultRoleProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />

      </providers>

    </roleManager>

    <sessionState mode="InProc" customProvider="DefaultSessionProvider">

      <providers>

        <add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" applicationName="/" />

      </providers>

    </sessionState>

  </system.web>

</configuration>

Básicamente redefinen los proveedores nativos y añaden nuevas líneas para os proveedores contenidos en el ensamblado System.Web.Providers.

Hay dos cadenas de conexión que por defecto apuntan a la misma base de datos de SQL Server Express:

  • ApplicationServices: es la cadena de conexión que usaremos en caso de emplear los proveedores nativos. Si nuestra intención es usar los nuevos "universales" entonces podemos borrarla directamente.
  • DefaultConnection: es la que usaremos con los proveedores universales.

Esta última, por defecto, apunta a la base de datos aspnetdb.mdf que se crea automáticamente en la carpeta App_Data por parte de ASP.NET y por lo tanto si no la modificamos obtendremos el mismo comportamiento que los proveedores nativos.

Si quisiésemos emplear una base de datos de Windows Azure bastaría con cambiar la cadena de conexión y poner algo similar a esto:

<add name="DefaultConnection" connectionString="Data Source=miSqlAzure;InitialCatalog=miBBDD;UserID=miUsuario;Password=miClave;Encrypt=true;Trusted_Connection=false;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" />

Como vemos es prácticamente idéntica a la cadena de conexión de una base de datos de SQL Server pero cambiando el nombre del servidor por la URL de nuestra instancia de SQL Azure.

Como ejemplo vamos a utilizar SQL Server Compact y veremos la base de datos resultante.

Cambia la cadena de conexión por algo como esto:

<add name="DefaultConnection" connectionString="Data Source=|DataDirectory|aspnet.sdf" providerName="System.Data.SqlServerCe.4.0" />

Ahora compila la aplicación antes de nada (Menú "Build" y luego "Build nombre de tu proyecto"):

ProveedoresUniversales_5

A continuación pulsa el botón de "Configuración de ASP.NET" en el explorador de soluciones o bien en el menú "Proyecto":

ProveedoresUniversales_6

Pulsa para aumentar

Esto abrirá la aplicación Web de configuración de tu aplicación. Ahora ve a la parte de "Seguridad" y habilita la funcionalidad de "Roles":

ProveedoresUniversales_7

Pulsa para aumentar

Vete a la pestaña "Provider" ("Proveedor" en castellano) y elige la opción "Seleccionar un proveedor diferente para cada características (avanzado)". Verás que cada API tiene asignada el proveedor "Default" que es el universal:

ProveedoresUniversales_8

Vuelve a "Seguridad" y crea un rol o dos, así como un par de usuarios. Cierra la configuración.

En el explorador de soluciones pulsa elbotón de ver todos los archivos y busca dentro de la carpeta "App_Data" un nuevo archivo llamado "aspnet.sdf". Pulsa con el botón derecho sobre éste y elige la opción de "Incluir en el proyecto":

ProveedoresUniversales_9

Ahora ya lo tenemos en el proyecto y cuando lo despleguemos se moverá con él.

Para ver su contenido simplemente haz doble-clic sobre él. Se creará una nueva conexión en el explorador de datos y podremos examinar sus tablas y registros:

ProveedoresUniversales_10

Pulsa para aumentar

Por lo demás podremos utilizar Membership, Roles y Profile de la forma habitual, al igual que todos los controles de Login de la barra de herramientas, etc… sólo que la información en lugar de guardarse en un SQL Server convencional podrá almacenarse en SQL Server Azure o en SQL Server Compact.

Esperemos que más adelante Microsoft ofrezca más APIs y más opciones de almacenamiento, pero este es un buen primer paso en el buen camino.

¡Espero que te resulte útil!