ASP.NET MVC en IIS 6

Desde hace unos días estuve intentando hacer andar ASP.NET MVC sobre IIS 6, hay un problema documentado por el cual nos encontramos con el error “The Page cannot be found” cuando queremos acceder a una determinada View

step0 - pagenotfound

Luego de recorrer y probar varias soluciones de lo mas diversas que iban desde modificar el código fuente de mi aplicación a configurar el IIS me quedé con esta que les cuento a continuación.

Pasos para hacer funcionar ASP.NET MVC sobre IIS 6

1. En las propiedades del sitio (o el directorio virtual) vamos a la sección “Application settings” y entramos en “Configuration”

step1

2. en “Wildcard application maps” vamos a “Insert” y agregamos “c:\windows\microsoft.net\framework\v2.0.50727\aspnet_isapi.dll” (o “Edit” en caso de que exista)

step2

3. y ATENCIÓN a que NO esté tildada la opción “Verify that file exists”

step3

Finalmente aceptamos todas las ventanas que fuimos abriendo y el sitio debería estar funcionando.

referencias: http://www.google.com.ar/search?q=asp.net+mvc+on+iis+6

Integración Continua (CI)

Pensando la integración continua

La integración continua consiste en verificar que todo sigan funcionando mediante la compilación y ejecución de test en forma desatendida. es una práctica altamente recomendada cuando se trabaja en equipos de desarrollo y sobre todo muy positiva cuando los equipos son distribuidos.

También se extiende a la generación desatendida de builds, deploy a QA, métricas como puede ser el coverage, etc.

Armando una integración continua

En este caso vamos a usar:

No voy a hacer un step by step de como configurar cada una pues depende de las variantes que se quieran instalar en cada caso particular, pero cualquier duda concreta podés dejar un mensaje y la vemos.

Armando proyectos en Cruise Control

primero vamos a definir nuestro proyecto:

<project name="dms.ci" queue="dms">

donde name es el nombre por el que nos vamos a referir a este proyecto, queue es la cola de trabajo en la que participa el proyecto, esto sirve para agrupar los proyectos que NO se quieren ejecutar simultáneamente.

cada proyecto se dividen en cuatro partes principales:

1. triggers: definición de cuando vamos a disparar las tareas y ante que condiciones

En este caso estamos actualizando el proyecto cada 5 minutos y si hay novedades ejecutamos las tareas.

<triggers>
    <
intervalTrigger seconds="3600" initialSeconds="1" />
</
triggers>

O el famoso Nightly build, que en este caso se ejecuta de lunes a viernes a las 4 am.

<triggers>
    <scheduleTrigger time="4:00">
        <weekDays>
            <weekDay>Monday</weekDay>
            <weekDay>Tuesday</weekDay>
            <weekDay>Wednesday</weekDay>
            <weekDay>Thursday</weekDay>
            <weekDay>Friday</weekDay>
        </weekDays>
    </scheduleTrigger>
</triggers>
2. sourcecontrol: obtención de las novedades del código fuente

En este caso las obtenemos desde el repositorio svn de google code.

<sourcecontrol type="svn">
    <trunkUrl>https://dmsneluz.googlecode.com/svn/trunk</trunkUrl>
    <workingDirectory>c:\cruisecontrol\dms\CI</workingDirectory>
</sourcecontrol>
3. tasks: tareas a ejecutar

En este caso se trata de una tarea de MSBuild escrita en el archivo src/dms.msbuild cuyo target es Testing. (ver: lista (in)completa de tareas)

<tasks>
    <msbuild>
        <executable>C:\Windows\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
        <workingDirectory>c:\cruisecontrol\dms\CI</workingDirectory>
        <projectFile>src\dms.msbuild</projectFile>
        <buildArgs>/noconsolelogger</buildArgs>
        <targets>Testing</targets>
        <timeout>900</timeout>
        <logger>C:\Program Files\CruiseControl.NET\server\ThoughtWorks.CruiseControl.MsBuild.dll</logger>
    </msbuild>
</tasks>
4. publishers: informar los resultados

En este caso estamos agregando al reporte del CruiseControl el contenido de Reports\nunit-test.xml que es donde tenemos el resultado de la ejecución de los test que posteriormente va a ser vizualizado en el dashboard de ccnet.

<publishers>
    <merge>
        <files>
            <file>c:\cruisecontrol\dms\CI\Reports\nunit-test.xml</file>
        </files>
    </merge>

    <xmllogger />
</publishers>

Dashboard del proyecto de ejemplo

ccnet

hay una copia de los archivos de configuración actuales de cc en el repositorio del ejemplo, en la carpeta ccnet.

Post relacionado: Coverage con PartCover en un servidor de CI

Estrategias para manejar los datos de prueba

Voy a intentar recopilar las distintas formas que he usado para manejar los datos de prueba en distintos desarrollos (y algunas veces también para las entregas parciales de demos de un producto) y compararlas desde las experiencias que tuve.

La hipótesis: “la base de datos va madurando a lo largo de las iteraciones del desarrollo”, esto en contraposición a hacer primero una base de datos y luego un desarrollo guiado por la base de datos.

1. Simplemente una base de datos

Esta es la más básica, creamos la base de datos de nuestra aplicación y la vamos llenando de datos manualmente así como modificando su estructura, si trabajamos en equipo la tenemos en un servidor y todos modificamos la base de datos. Esto quizás sea mas bien una falta de estrategia o “anti-estrategia”.

Desventajas:

  • Cuando alguien hace una modificación impacta en la db en línea que usa el resto del equipo, quienes posiblemente aún no tengan las actualizaciones de código correspondiente.
  • Si se quiere deshacer una modificación, se debe considerar como otro cambio pues que la db ya fue modificada.
  • Van quedando datos obsoletos a medida que se refactoriza nuestra aplicación, práctica necesaria (por no decir obligatoria) en las metodologías ágiles.
  • La aplicación es mono-db.

2. Scripts

Tenemos una batería de scripts que crean la estructura de la db e insertan los datos de prueba. Una variante de esto es tener la estructura de tablas en script y los datos en xml con un proceso que los inserte.

Ventajas:

  • Permite tener un control de cambios al tener versionados los scripts (o si hacemos scripts incrementales).
  • Permite que cada desarrollador trabaje con db locales (y no es necesaria una db en línea compartida)

Desventajas:

  • No se cuenta con las mismas validaciones de datos que en la aplicación, por lo que con los cambios de validaciones también pueden quedar datos obsoletos como en la estrategia anterior.
  • Los scripts dependen del repositorio donde guardemos los datos.

3. Objetos

Tenemos factorías de objetos (o conjuntos de objetos) los cuales se guardan cada vez que es necesario, usando los mismos componentes de persistencia que la aplicación, pero antes se regenera la estructura de la db de cero utilizando alguna herramienta para esto.

Ventajas:

  • Nos permite tener un control de cambios al tener versionado el código fuente.
  • Nos permite que cada desarrollador trabaje con db locales.
  • Tenemos las mismas validaciones que en nuestra aplicación. No hay datos obsoletos.

Desventajas:

  • Es más lento por estar recreando a cada rato la db.

4. Repositorios en memoria

A la opción de “Objetos”, le sumamos usar repositorios en memoria, que guarden nuestros objetos en una lista.

Ventajas:

  • Nos permite no tener que definir la estructura de la base de datos hasta avanzado el desarrollo.
  • Nos permite tener un control de cambios al tener versionado el código fuente.
  • No hay db en línea ni local que tengamos que actualizar.
  • Tenemos las mismas validaciones que en nuestra aplicación. No hay datos obsoletos.
  • Es mas rápido pues no hay que preparar la base de datos para cada ejecución.

Desventajas:

  • Hay que programar estos repositorios en memoria, código que posteriormente va a ser descartado.

Resumen

  Una única DB Scripts Scripts y XML Objetos Repositorios en memoria
Trabajo en equipo -- SI SI SI SI
Control de cambios -- SI SI SI SI
Validación -- -- -- SI SI
Performance alta media media baja alta
Refactoring costoso costoso costoso fácil fácil
Código descartable ninguno poco poco poco un poco más

Construyendo una UI

¿que tan difícil puede ser construir una interface de usuario web para alguien que no sabe mucho del tema?

Poco. Siguiendo con la idea de siempre, delegamos esto a algún experto, el único detalle en este caso es que no conozco ninguno que sea de uso gratuito y que llegue a los niveles de los productos comerciales.

En este caso vamos a usar DevExpress en su modo evaluación, por lo que vamos a tener una leyenda en la parte superior de nuestras páginas advirtiendo esto.

Como primer UI vamos a hacer un sitio web, vamos a ver como con pocas líneas de nuestro código logramos mucha funcionalidad.

El objetivo

CustomersView

Este formulario vamos a dividirlo en dos partes

  • Master Page: el título (“Digital Media Store”), el combo para seleccionar la apariencia, el menú de la izquierda y el pié de página.
  • CustomersView: el filtro por país y la grilla.

En esta última es que vamos a centrarnos, como podemos ver, además del filtro por país, la grilla tiene la funcionalidad de filtro por cada una de las columnas (eso es la primer fila con textbox vacios). También tiene un botones para editar, crear y eliminar customers. tiene la opción de agrupar por columnas (“Drag a column header here to group by that column”), ordenar, pagina los datos, y todo esto con AJAX.

pregunta: ¿cuantas líneas de código necesitamos para hacer esto, incluyendo los formularios de edición/alta?. Respuesta: 23 líneas en el aspx, 0 (cero) en el code behind y 16 de código C# en una clase. Veámoslas…

El código

El filtro por país

un label, un textbox y un button que solo dispara un postback

<table>
<
tr>
<
td><dxe:ASPxLabel ID="lblContry" Text="Country" runat="server" ></dxe:ASPxLabel></td>
<
td><dxe:ASPxTextBox ID="txtCountry" runat="server" Text="France" Width="170px"></dxe:ASPxTextBox></td>
<
td><dxe:ASPxButton ID="btnSearch" runat="server" Text="Search"></dxe:ASPxButton></td>
</
tr>
</
table>

La grilla:

una grilla de DevExpress

<dxwgv:ASPxGridView ID="gridCustomers" runat="server" DataSourceID="odsCustomers" KeyFieldName="Id" Width="100%">
<
Columns>
<
dxwgv:GridViewCommandColumn>
<
EditButton Visible="true" />
<
NewButton Visible="True" />
<
DeleteButton Visible="True" />
</
dxwgv:GridViewCommandColumn>
<
dxwgv:GridViewDataColumn FieldName="FirstName" />
<
dxwgv:GridViewDataColumn FieldName="LastName" />
<
dxwgv:GridViewDataColumn FieldName="Country" />
<
dxwgv:GridViewDataColumn FieldName="State" />
<
dxwgv:GridViewDataColumn FieldName="City" />
</
Columns>
<
Settings ShowFilterRow="True" ShowGroupPanel="true"/>
<
SettingsBehavior ConfirmDelete="true" />
<
SettingsEditing EditFormColumnCount="1" Mode="PopupEditForm" PopupEditFormModal="true" PopupEditFormHorizontalAlign="Center" PopupEditFormVerticalAlign="Middle" />
</
dxwgv:ASPxGridView>

veamos las propiedades significativas:


  • DataSourceID para vincularnos al data source,
  • KeyFieldName es en nombre de la property que identifica a cada objeto (en este caso es Id)
  • Columns es la lista de columnas que va a tener la grilla, empezando por las de comando: EditButton, NewButton y DeleteButton y siguiente por las propiedades que queremos mostrar en la grilla y/o en los formularios de edición.
  • ShowFilterRow muestra la primer fila donde están los campos para el filtrado.
  • ShowGroupPanel muestra el panel de agrupamiento.
  • ConfirmDelete pide una confirmación al intentar borrar un registro.
  • Mode indicamos el modo en que queremos que aparezca la pantalla de alta y edición de nuevos customers.
  • PopupEditFormModal indica que el popup va a ser modal, es decir que no se va a poder usar otra parte de la página mientras se esté editando.

y esto es todo para obtener la funcionalidad citada: filtrar, editar, crear, eliminar, agrupar, ordenar y paginar (estas dos últimas están activas por default).

El DataSource

un ObjectDataSource de asp.net:

<asp:ObjectDataSource ID="odsCustomers" runat="server" DataObjectTypeName="DMS.DTOs.CustomerDTO, DMS.DTOs" TypeName="DMS.Web.DataSources.CustomerDataSource"
SelectMethod="Select" InsertMethod="Save" UpdateMethod="Save" DeleteMethod="Delete">
<
SelectParameters>
<
asp:ControlParameter Name="country" PropertyName="Text" ControlID="txtCountry" />
</
SelectParameters>
</
asp:ObjectDataSource>

veamos las propiedades significativas:


  • DataObjectTypeName indica el tipo de objeto que vamos listar en la grilla, en este caso un CustomerDTO.
  • TypeName es el tipo de objeto que vamos a usar como datasource y los métodos para cada acción: SelectMethod, InsertMethod, UpdateMethod, DeleteMethod.
  • SelectParameters son los parámetros que tiene el método asociado a SelectMethod, en este caso se llama country y el valor lo tomamos de la propiedad “Text” del control “txtCountry”.

El código del DataSource:
public class CustomerDataSource
{
private readonly ICustomerService service = ServiceLocatorHelper.GetService<ICustomerService>();

public IList<CustomerDTO> Select(string country)
{
ResultListDTO<CustomerDTO> result = service.GetCustomersByContry(country);
if (result.Exception != null)
throw result.Exception;
return result.Items;
}

public void Save(CustomerDTO dto)
{
service.Save(dto);
}

public void Delete(CustomerDTO dto)
{
service.DeleteById(dto.Id);
}
}

Aquí simplemente hacemos llamadas al servicio de manejo de customers y manejamos los valores de retorno. Aún me falta una vuelta de rosca para el manejo de mensajes, pero en breve espero que esté.

Y esto es solo una mínima muestra de lo que se puede hacer con DevExpress

Cómo no odiar a la session de NHibernate

6 pasos para llega a odiar la session de nh

Primer paso: pensar y usar la session de nh (de ahora en mas simplemente ‘session’) como si sólo fuese la una conexión a base de datos, es decir que la abrimos, la usamos y la cerramos.

Segundo paso: cuando empiezan las “LazyInitializationException” ponemos los lazy en false en un intento de que no lance más esa odiosa excepción.

Tercer paso: cuando queremos actualizar objetos empezamos a recibir “PersistentObjectException: detached entity passed to persist” o “HibernateException: reassociated object has dirty collection”. y aquí es donde tendemos a dejar una session abierta y usarla en toda la aplicación. Quizás no nos damos cuenta, pero ya podríamos volver a poner el lazy en true.

en este momento bastaría con poner “LazyInitializationException” o “PersistentObjectException: detached entity” o “NHibernate reassociated object has dirty collection” en google para saber que no estamos solos, ni somos los primeros en encontrarnos con este problema.

Cuarto paso: si seguimos con los lazy en false, vamos a tener problemas de performance cuando el volumen de datos sea considerable (afectando así a la escalabilidad), vamos a pedir una factura (para consultar la fecha por ejemplo) y nos va a cargar las líneas de la misma, sus tracks, su álbum, su artista, los otros tracks del álbum, etc. Aquí es donde decimos que “NHibernate no sirve para aplicaciones con un dominio complejo” o “no sirve para grandes aplicaciones como la nuestra”.

Quinto paso: si en algún momento optamos por una session dejándola abierta, se vuelve a complicar cuando la aplicación corre en múltiples hilos, como cuando un sitio web entra en testing ya que hay usuarios concurrentes, cosa que no pasaba en el ambiente de desarrollo local de cada PC. Aquí ya estamos “complicados”.

Sexto extra: como frutilla del postre, luego de que la session nos lanza una excepción de, por ejemplo, validación, notamos que la session empieza a tener un comportamiento extraño. No nos guarda los objetos que le decimos que nos guarde. Aquí llegamos al odio!

Algunos otros síntomas también pueden ser el uso de Session.Evict, Session.Clear, Session.Flush por todo nuestro código. Aquí se declaró la guerra!

Posiblemente pensemos que no tenemos idea que es la session, y quizás sea verdad, quizás no tengamos idea de que es la session. Aquí empiezan las soluciones, empezamos a buscar en google como se debe manejar la session de NH, encontramos algunos frameworks que lo hacen y un montón de post al respecto de este tema, quizás así llegaste a este.

Empezando el camino de la reconciliación

Lo primero que tenemos que hacer es entender qué es esta session. Cuando más o menos entendamos esto, vamos a poder decidir como manejarla. Sino vamos a estar pensando que manejamos un auto y estamos piloteando a un avión (con 200 pasajeros a bordo) y nos vamos a dar cuenta cuando estemos carreteando.

Algunos tips sobre que es la session:
- la session tiene información del estado original de los objetos, que luego va a usar para saber si los mismos fueron modificados.
- la session tiene una caché que nos garantiza que existe sólo una instancia de cada objeto dentro de ésta, y que no a volver a buscar en la base de datos un objeto que ya consultó a menos que se lo indiquemos específicamente.
- la session va a volver a ser usada cuando se carguen los lazy loading (ver ejemplo más abajo).

Podemos pensar a la session de nh como una conversación entre los objetos de nuestra aplicación y nuestra base de datos. Esta conversación puede durar lo que una llamada a un método, lo que un request dentro de un entorno web o estar asociada al ciclo de vida de otro objeto, etc. Lo importante en este punto es entender que el ciclo de vida de la session de nh depende del contenido de la conversación, no podemos cortar la conversación, seguir hablando y pretender entendernos. Si queremos introducir un objeto a una conversación (session) diferente a la que lo originó hay técnicas especificas para esto (reattach). Así que o trabajamos con la misma session o hacemos un reattach.

Si logramos entender este concepto, vamos a saber cuanto tiene que durar nuestra conversación, vamos a entender el ciclo de vida que debe tener nuestra session. Y aquí es donde podemos optar por algún framework que lo haga por nosotros, pero siempre le vamos a tener que decir qué ciclo de vida queremos que tenga en cada caso.

framworks que administran la session por nosotros: los mismos Context de NHibernate (como WebSessionContext y ThreadStaticSessionContext), unhaddinsNHibernate.Burrow, Castle, Spring.NET, Rhino Tools, etc.

Entendiendo el Lazy Loading con ejemplos

   1: Invoice invoice;
   2: using (ISession session = sessionFactory.OpenSession())
   3: {
   4:     invoice = session.Get<Invoice>(id);
   5:     Assert.IsNotNull(invoice);
   6:  
   7:     Assert.IsNotNull(invoice.Customer);
   8:     Assert.Greater(invoice.Customer.Id, 0);
   9: }
  10:  
  11: string firstName = string.Empty;
  12: Assert.Throws<LazyInitializationException>(() => firstName = invoice.Customer.FirstName);
  13:  
  14: Assert.IsEmpty(firstName);




como podemos ver en este test, en la línea 9 estamos cerrando la session (el dispose del using) y en la línea 12 estamos confirmando una excepción (de tipo LazyInitializationException) al intentar acceder a la propiedad FirstName de la propiedad Customer de Invoice. Esto significa que la propiedad Customer no fue cargada aún (de la base de datos) y necesitaba la session de nh para hacerlo, la misma session con que fue cargado el objeto. Además, como pueden observar, en las líneas 7 y 8 estamos haciendo uso de esta propiedad, pero como no es necesario (todavía) ir hasta la base de datos por estos datos es que nhibernate no los carga.

¿cuál sería el ciclo de vida correcto para este ejemplo?


   1: using (ISession session = sessionFactory.OpenSession())
   2: {
   3:     Invoice invoice = session.Get<Invoice>(id);
   4:     Assert.IsNotNull(invoice);
   5:  
   6:     Assert.IsNotNull(invoice.Customer);
   7:     Assert.Greater(invoice.Customer.Id, 0);
   8:  
   9:     string firstName = invoice.Customer.FirstName;
  10:  
  11:     Assert.AreEqual("Nelo", firstName);
  12: }


El objeto invoice (y los objetos relacionados por sus propiedades) se usan dentro del ciclo de vida de la session.

Les dejo una lectura recomendada (obligatoria diría) aquí.