lunes, 15 de abril de 2013

Rompiendo el hielo con EF Code First

Hay mucha gente para la que el enfoque Code First de Entity Framework es natural e intuitivo: crear un modelo a partir de clases POCO y delegar en Entity Framework para la creación automática de una base de datos que pueda persistir el modelo. Sin embargo, hay otra mucha gente (yo me incluyo entre ellos) que está acostumbrada a crear primero la base de datos y después crear el modelo (esta vez apoyándose en el famoso fichero .edmx y con el enfoque Database First).

Siendo así, me gustaría que este post sirviera a escépticos de Code First que aún están indecisos sobre si este enfoque es o no viable (como la estaba yo hace unas pocas semanas).

Lo primero es lo primero, exactamente y en pocas palabras ¿Qué es Code First?

Code First es un enfoque más de Entity Framework (hay otros dos enfoques que son Database First y Model First) que plantea lo siguiente: Tú crea clases POCO con tu lenguaje favorito (C#, VB.NET, etc.) y crea relaciones entre las mismas, después despreocúpate que ya veré yo como me las gasto para persistir tu modelo en una base de datos.

Lo importante es entender que con Code First, lo primero es el código. En vez de comenzar creando la base de datos y después con ingeniería inversa generar las clases POCO (como hacíamos con Database First), con Code First primero creamos el modelo con código y después se genera automáticamente la base de datos. Lo cierto es que aunque el espíritu de Code First es el que he explicado, Code First también puede trabajar con base de datos existentes, puedes ver más información en Code First to an Existing Database.

Asumiendo para el resto del post que la base de datos se creará automáticamente, Code First nos reporta las siguientes ventajas:

  • En nuestro proyecto sólo hablamos de código, no hablamos más de bases de datos ni de T-SQL, al fin y al cabo, la base de datos será simplemente un forma más de persistir nuestro modelo (la base de datos será un medio, no el fin).
  • Parece que, definitivamente, resolvemos el problema del desajuste de impedancia. Esto es que nuestro proyecto ya no vive en dos mundos, el de la base de datos y el del código, ya no tenemos que saber T-SQL además de C#, ya sólo con nuestras clases POCO y Linq podremos abordar cualquier proyecto.

Cierto es que estas ventajas también las conseguimos con Database First y Model First, porque al fin y al cabo un modelo EDM en memoria es siempre el mismo con independencia del enfoque del que provenga. Sin embargo, con Code First todo parece más fluido y, con un poco de suerte, sólo abrirás la consola de administración de Sql Server para confirmar que tu modelo ha persistido y que no es todo una ilusión y tus datos están en el limbo.

Lo siguiente sería ver un ejemplo para poner cara y ojos a nuestro amigo Code First:

1. Crear un nuevo proyecto del tipo Aplicación web de ASP.NET MVC 4 y llamarlo Tienda.

2. Agregar un nuevo proyecto de Biblioteca de clases a la solución y llamarlo Models.

3. Agregar una referencia a Models desde Tienda.

4. Agregar al proyecto Models el paquete Entity Framework desde NuGet (asumimos que el proyecto Tienda ya tendrá instalado el paquete al seleccionar alguna de las plantillas de ASP.NET MVC).

5. Agregar el siguiente código al proyecto Models, con el que estamos definiendo nuestro modelo sólo a través de código y clases POCO “ignorantes de la persistencia”:

using System;

using System.Collections.Generic;

using System.Data.Entity;

 

namespace Models

{

    public class TiendaContext : DbContext

    {

        public DbSet<Cliente> Clientes { get; set; }

        public DbSet<Producto> Productos { get; set; }

        public DbSet<Pedido> Pedidos { get; set; }

        public DbSet<LineaPedido> LineasPedido { get; set; }

    }

 

    public class Cliente

    {

        // Comentar si ProxyCreationEnabled = true

        public Cliente()

        {

            Pedidos = new Collection<Pedido>();

        }

        public virtual int ClienteId { get; set; }

        public virtual string Nombre { get; set; }

        public virtual ICollection<Pedido> Pedidos { get; set; }

    }

 

    public class Producto

    {

        // Comentar si ProxyCreationEnabled = true

        public Producto()

        {

            Lineas = new Collection<LineaPedido>();

        }

        public virtual int ProductoId { get; set; }

        public virtual string Descripcion { get; set; }

        public virtual decimal Precio { get; set; }

        public virtual ICollection<LineaPedido> Lineas { get; set; }

    }

 

    public class Pedido

    {

        // Comentar si ProxyCreationEnabled = true

        public Pedido()

        {

            Lineas = new Collection<LineaPedido>();

        }

        public virtual int PedidoId { get; set; }

        public virtual DateTime FechaCreacion { get; set; }

        public virtual int ClienteId { get; set; }

        public virtual Cliente Cliente { get; set; }

        public virtual ICollection<LineaPedido> Lineas { get; set; }

    }

 

    public class LineaPedido

    {

        public virtual int LineaPedidoId { get; set; }

        public virtual int PedidoId { get; set; }

        public virtual int ProductoId { get; set; }

        public virtual int Unidades { get; set; }

        public virtual Pedido Pedido { get; set; }

        public virtual Producto Producto { get; set; }

    }

}

Lo de “ignorantes de la persistencia” debería ir entrecomillado. Digo esto porque EF podría crear proxys dinámicos en tiempo de ejecución a partir de nuestras clases POCO para soportar la carga diferida y el seguimiento de cambios. Los requisitos para la creación automática de estos proxys los puedes consultar en el siguiente enlace Requisitos para crear objetos proxy POCO (Entity Framework).

A grandes rasgos, para habilitar la creación automática de proxys de carga diferida debemos especificar virtual en todas las propiedades de navegación (tanto de referencia como de colección) y establecer a true las propiedades LazyLoadingEnabled y ProxyCreationEnabled (por defecto están activadas).

Para los proxys de seguimiento de cambios, todas las propiedades de nuestra clase deben ser virtual y las propiedades de navegación de colección tienen que ser ICollection (las cuales serán convertidas después a EntityCollection<TEntity>). Además tiene que valer true la propiedad ProxyCreationEnabled (fíjate que aquí no hay una propiedad específica para habilitar o deshabilitar la creación de estos proxys como sí la hay para los proxys de carga diferida).

En lo relativo a inicializar las propiedades de navegación de colección en el constructor de la clase, sirve para cuando no generamos ningún tipo de proxy y queremos inicializar la colección. Si este código está presente y está activa la generación de proxys de seguimiento de cambios se producirá una excepción (no así con los proxys de carga diferida, aunque en este caso tampoco sería necesario ese código). Con una versión más reducida y simple (asumiendo valores por defecto para la configuración de EF), por ejemplo la clase Pedido quedaría así:

    public class Pedido

    {

        public int PedidoId { get; set; }

        public DateTime FechaCreacion { get; set; }

        public int ClienteId { get; set; }

        public virtual Cliente Cliente { get; set; }

        public virtual ICollection<LineaPedido> Lineas { get; set; }

    }

 

En cuanto a si es o no conveniente la creación automática de proxys, hay todo un debate abierto con gente a favor y gente en contra. Yo personalmente deshabilito la creación de cualquier tipo de proxy (con ProxyCreationEnabled igual a false). Ya no :)

Estos proxys serán del tipo System.Data.Entity.DynamicProxies.EntityType_…, si por algún motivo se quisiera saber el tipo base en tiempo de ejecución (el tipo original de la entidad) podemos obtenerlo con ObjectContext.GetObjectType(entity.GetType()).

6. Por último, simplemente utilizar el modelo a través de la clase de contexto y quedar atónitos con la magia de Code First…

using (var context = new TiendaContext())

{

    var cliente = new Cliente() { Nombre = "Sergio" };

    context.Clientes.Add(cliente);

    context.SaveChanges();

}

Que ha generado automáticamente la base de datos Models.TiendaContext en nuestra instalación de SQL Express con las siguientes tablas y relaciones:

clip_image001[4]

Si ya conocías Code First no estarás muy impresionado (lo entiendo, no te culpo), pero si no el caso, me gustaría ver la cara de asombro y perplejidad que se te ha quedado al ver que una nuevo base de datos se ha creado automáticamente con todo lo necesario para persistir tu modelo y sin haber tirado ni una sola línea de T-SQL ¿No podrás negar que ha sido alucinante?

A partir de aquí, al menos yo tuve muchas preguntas:

  • ¿Por qué se creó la base de datos en SQL Express y por qué con ese nombre?
  • ¿En qué momento se creó la base de datos?
  • ¿Qué pasa si el modelo cambia?
  • ¿Qué pasa si la estructura de base de datos no me convence, puedo cambiar algo?

Si vienes de Database First o es tu primera vez con Code First, estoy seguro que estas y otras muchas preguntas te estarán asaltando en este preciso instante. Pues nada, vamos a ello.

Lo primero es entender cómo funciona Code First.

El primer paso que lleva a cabo Code First es leer las clases accesibles en la clase de contexto y crear un modelo en memoria. A continuación infiere un esquema de base de datos válido para persistir el modelo. Para esta tarea, Code First se basa en convenciones, por ejemplo si nuestra clase tiene una propiedad <NombreClase>Id o simplemente Id, esta propiedad será elegida para ser la clave primaria en la base de datos. Si además es numérica, creará el campo clave como autonumérico, etc.

¿Qué cuáles son exactamente las convenciones de Code First para inferir el esquema de base de datos? Pues son muchas, todas ellas en el espacio de nombres System.Data.Entity.ModelConfiguration.Conventions, las puedes consultar aquí, pero a grandes rasgos las más relevantes que se han utilizado para crear nuestro modelo han sido las siguientes:

  • El nombre de la base de datos ha sido el nombre del contexto plenamente cualificado, es decir, Models.TiendaContext.
  • La base de datos se ha creado en la instancia de SQL Express.
  • El nombre de la tabla será el nombre de la clase en plural. Como el servicio de pluralización sólo está disponible en inglés, tenemos nombres tan divertidos y ocurrentes como “Pedidoes”. Lógicamente esto no es muy acertado y después veremos cómo solucionarlo.
  • La clave primaria de cada tabla ha sido <NombreClase>Id.
  • Las claves primarias son todas autonuméricas.
  • A partir de las propiedades de navegación y propiedades de clave externa que encontró en el modelo, creó en base de datos las relaciones oportunas. Code First reconoció las relaciones porque los nombres de las mismas siguieron las convenciones predeterminadas. Por ejemplo:
    • Clientes.Pedidos es una propiedad de navegación de colección.
    • Pedidos.Cliente es una propiedad de navegación de referencia
    • Pedidos.ClienteId es una propiedad de clave externa que será utilizada para llevar a cabo la relación en base de datos entre Pedidos y Clientes.

Como podemos ver, la mayoría de las convenciones de Code First parecen muy razonables y pienso que merece la pena cumplirlas para trabajar lo menos posible. Sin embargo, puede haber situaciones en las que alguna convención no nos satisfaga (como por ejemplo el nombre en plural de las tablas) o bien no podamos cumplirla por algún motivo (imagina por ejemplo que el campo Pedidos.ClienteId tuviera que ser creado en código con el nombre Pedidos.ClienteAsociadoId).

En estos casos, lo que necesitamos es poder invalidar las convenciones predeterminadas de Code First e instruirle de forma precisa sobre cómo queremos que se realicen ciertos mapeos. Es decir, estamos tomando el control y decidiendo por nosotros mismos como inferir el esquema. Además de poder eliminar convenciones, lo más habitual es realizar ajustes a través de alguna de estas vías:

  • DataAnnotations.
  • Fluent API.

Cuando elegir una u otra es un tema de profundo debate, pero a grandes rasgos:

  • DataAnnotations es más sencillo de utilizar que Fluent API.
  • Con DataAnnotations hay ciertos escenarios que no podemos conseguir.
  • Con DataAnnotations “ensuciamos” en cierto modo nuestras clases POCO, tanto a la vista del desarrollador como a efectos de interoperabilidad.
  • Con Fluent API tenemos más control y además es más cool (vale, lo he dicho y no me arrepiento).

Al respecto de los ajustes, la precedencia que aplica Code First es primero Fluent API, después DataAnnotations y por último las convenciones predeterminadas.

Dicho esto, para ajustar nuestro modelo veremos ambas opciones y así después podremos decidir.

Los ajustes que realizaremos a modo de ejemplo sobre la entidad Pedido serán:

  • Elegir el nombre “Pedidos” para la tabla.
  • Crear el campo PedidoId como no autonumérico (queremos controlar el número de pedido).
  • La propiedad de clave externa con Clientes tiene que llamarse ClienteAsociadoId en nuestro modelo pero tiene que continuar llamándose ClienteId en la base de datos (está claro que este requerimiento es un poco absurdo, pero nos servirá para el ejemplo).

Para ajustar el modelo con DataAnnotations tendremos que agregar una referencia al ensamblado System.ComponentModel.DataAnnotations en nuestro proyecto Models y decorar la clase Pedido con los siguientes atributos:

    [Table("Pedidos")]

    public class Pedido

    {

        [DatabaseGenerated(DatabaseGeneratedOption.None)]

        public int PedidoId { get; set; }

        public DateTime FechaCreacion { get; set; }

        [ForeignKey("Cliente")]

        [Column("ClienteId")]

        public int ClienteAsociadoId { get; set; }

        public Cliente Cliente { get; set; }

        public virtual List<LineaPedido> Lineas { get; set; }

    }

Si esto mismo lo hiciéramos con Fluent API, tendríamos que agregar una referencia en Models para el ensamblado System.Data.Entity y escribir los ajustes en el método OnModelCreating de nuestra clase de contexto:

protected override void OnModelCreating(DbModelBuilder modelBuilder)

{

    // Eliminar convención de pluralización de nombres de tablas

    modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

 

    modelBuilder.Entity<Pedido>().ToTable("Pedidos");

    modelBuilder.Entity<Pedido>()

                .Property(p => p.PedidoId)

                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

    modelBuilder.Entity<Pedido>()

                .Property(p => p.ClienteAsociadoId)

                .HasColumnName("ClienteId");

    modelBuilder.Entity<Pedido>()

                .HasRequired(p => p.Cliente)

                .WithMany(p => p.Pedidos)

                .HasForeignKey(p => p.ClienteAsociadoId);

}

A priori Fluent API parece mucho código para tan poco requerimiento, además parece algo complicado pero, lo cierto es que, después de entender cómo funciona y con algo de práctica, nos daremos cuenta que no se llama API Fluida por nada, realmente fluirá y nos resultará sencillo realizar ajuste por esta vía con poco esfuerzo.

En este punto, algo interesante es saber cómo podríamos gestionar estos ajustes para no ensuciar las clases (si hablamos de DataAnnotations) o no tener un método OnModelCreating de 1000 líneas (si hablamos de Fluent API).

Para el caso de DataAnnotations podríamos utilizar las famosas clases buddy (clase colega) para separar la clase POCO de los atributos.

    [MetadataType(typeof(PedidoMetadata))]

    public class Pedido

    {

        public int PedidoId { get; set; }

        public DateTime FechaCreacion { get; set; }

        public int ClienteAsociadoId { get; set; }

        public Cliente Cliente { get; set; }

        public virtual List<LineaPedido> Lineas { get; set; }

    }

 

    [Table("Pedidos")]

    public class PedidoMetadata

    {

        [DatabaseGenerated(DatabaseGeneratedOption.None)]

        public int PedidoId { get; set; }

        [ForeignKey("Cliente")]

        [Column("ClienteId")]

        public int ClienteAsociadoId { get; set; }

    }

En este ejemplo podemos ver:

·         Se ha indicado la clase buddy con el atributo MetadataType, en la clase Pedido.

·         En la clase PedidoMetadata (llamada así por convención pero podría haberse llamado de cualquier otra forma) hemos indicado los DataAnnotations sólo para las propiedades que queremos ajustar.

Con esto hemos conseguido separar la definición de nuestra clase de las DataAnnotations para Code First.

Si queremos lograr esta misma separación de conceptos a través de Fluent API, tendremos que crear una configuración para nuestra clase Pedido en una nueva clase que herede de EntityTypeConfiguration y registrarla durante el evento OnModelCreating.

Primero la clase de configuración:

public class PedidoConfiguration : EntityTypeConfiguration<Pedido>

{

    public PedidoConfiguration()

    {

        ToTable("Pedidos");

        Property(p => p.PedidoId)

                    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

        Property(p => p.ClienteAsociadoId)

                    .HasColumnName("ClienteId");

        HasRequired(p => p.Cliente)

                        .WithMany(p => p.Pedidos)

                        .HasForeignKey(p => p.ClienteAsociadoId);

    }

}

Después registrarla en el evento de creación del modelo:

protected override void OnModelCreating(DbModelBuilder modelBuilder)

{

    modelBuilder.Configurations.Add(new PedidoConfiguration());

}

Llegados a este punto, ya sabemos cómo se infiere el modelo y cómo podemos ajustarlo, así que ha llegado el momento de saber cuándo, cómo y dónde se crea la base de datos.

Por defecto, Code First intenta crear la base de datos en el primer acceso a la misma (en nuestro ejemplo anterior ocurrió cuando intentamos agregar un cliente a TiendaContext.Clientes). Además la intenta crear en la instancia de SQL Express (.\SQLEXPRESS) o en su defecto en LocalDb ((localdb)\v11.0), motor de base de datos que viene incluido con VS 2012. Respecto al nombre de la base de datos elegirá el nombre del contexto plenamente cualificado (Models.TiendaContext). Cómo lógicamente casi nunca nos servirá esta localización de base de datos, comencemos por ver cómo elegir donde crear la base de datos.

La forma más sencilla de elegir donde crear la base de datos es crear una cadena de conexión en nuestro fichero web.config/app.config con el nombre del contexto. Si hacemos esto también tendremos que agregar una referencia al paquete EF en el proyecto web para evitar el error “No Entity Framework provider found for the ADO.NET provider with invariant name 'System.Data.SqlClient'”, más info en http://stackoverflow.com/a/20045431

  <connectionStrings>

    <add name="TiendaContext"

         providerName="System.Data.SqlClient"

         connectionString="Data Source=(local)\sqlexpress;Initial Catalog=Tienda;Integrated Security=SSPI;MultipleActiveResultSets=True;Application Name=Tienda" />

  </connectionStrings>

De la cadena de conexión podemos comentar lo siguiente:

  • El atributo “name” puede ser el nombre de la clase de contexto o el nombre plenamente cualificado de la clase de contexto, es decir, valdría tanto TiendaContext como Models.TiendaContext.
  • No olvidar agregar el parámetro MultipleActieResultSets=True (necesario para el correcto funcionamiento de Entity Framework).
  • Agregar un nombre de aplicación si no quieres después sorpresas con TransationScope. Puedes ver más información en el post Buscando al culpable de Pedro Hurtado.

Claramente, para mi esta forma es la preferida porque después y con las transformaciones de ficheros Web.config podemos cambiar la cadena de conexión según la configuración activa (Debug o Release).

Otra forma de especificar donde conectará Code First es a través del constructor de DbContext.

    public class TiendaContext : DbContext

    {

        public TiendaContext()

        {

        }

        public TiendaContext(string nameOrConnectionString)

            : base(nameOrConnectionString)

        {

        }

    }

 

Ahora serían válidas todas estas combinaciones:

// Por defecto, cadena de conexión con el nombre TiendaContext

var context = new TiendaContext();

 

// Cadena de conexión con el nombre Tienda

// Sino existe creará una base de datos con el nombre Tienda

var context2 = new TiendaContext("Tienda");

 

// Cadena de conexión con el nombre Tienda

// Sino existe fallará

var context2 = new TiendaContext("Name=Tienda");

 

// Cadena de conexión explícita

var connectionString =

    @"Data Source=(local)\sqlexpress;"+

    "Initial Catalog=Tienda;"+

    "Integrated Security=SSPI;"+

    "MultipleActiveResultSets=True;"+

    "Application Name=Tienda";

var context3 = new TiendaContext(connectionString);

Ahora que ya sabemos especificar la localización exacta de la base de datos, todavía nos queda pendiente controlar el momento de la creación de la misma. Por defecto, Code First intentará crearla la primera vez que la necesite según la API de DbContext. En cualquier caso siempre podemos explícitamente decidir cuándo crearla con el método Initialize:

using (var context = new TiendaContext())

{

    context.Database.Initialize(false);

}

El objeto Database, además de Initialize nos suministra algunos otros métodos muy útiles:

·         CreateIfNotExists

·         CompatibleWithModel

·         Delete

·         Create

·         ExecuteSqlCommand

Especialmente interesante es el método CompatibleWithModel. Este método nos informa si el modelo actual es o no compatible con el esquema de la base de datos. Que devuelva true o false depende del valor asignado al parámetro throwIfNoMetada y de la existencia o no de la tabla __MigrationHistory y además con de una última fila que coincida para el campo ContextKey (es decir, no basta con ver que la tabla existe, tiene que tener algún modelo guardado para nuestro contexto según ContextKey). Esta tabla es creada por Code First cuando se crea automáticamente la base de datos o cuando se habilita Code First Migrations (ver el siguiente post que indica como crear esta tabla para una base de datos en la que no existe http://thedatafarm.com/blog/data-access/using-ef-migrations-with-an-existing-database/). Si quieres saber que guarda exactamente esta tabla, te recomienda la lectura de los siguientes posts Desmitificando Code Fisrt(1/2) y Desmitificando Code First(2/2) V 4.3.

CompatibleWithModel tiene las siguientes combinaciones:

throwIfNoMetada

Existe __MigrationHistory
/ContextKey

Resultado

false

No

true, no hay forma de comparar el modelo con el esquema y entonces se asume que es compatible.

false

Sí

true o false en función de si el modelo es o no compatible con el esquema.

true

No

Se lanzará una excepción porque no se han podido comparar modelo y esquema.

true

Sí

true o false en función de si el modelo es o no compatible con el esquema.

               

Sea como fuere, la pregunta que surge a continuación es ¿Qué pasa si el modelo cambia? Es decir, si el esquema de base de datos no es compatible con el modelo ¿Qué ocurrirá? Pues la respuesta a estas preguntas son las distintas estrategias de inicialización de Code First.

Por defecto, Code First incorpora las siguientes estrategias de inicialización:

Con CreateDatabaseIfNotExists, si la base de datos no existe se crea, y si existe y el modelo no es compatible se lanza un error (aunque puedes no tener __MigrationHistory y entonces devolverá siempre que la base de datos y el modelo son compatibles).

Con DropCreateDatabaseIfModelChanges la diferencia está en que si el modelo no es compatible con la base de datos, la misma se eliminara y se volverá a crear de nuevo (aquí es obligado tener __MigrationHistory porque si no lanzará una excepción).

Por último, con DropCreateDatabaseAlways siempre se elimina y se crea la base de datos, sin importar si es o no compatible con el modelo (lógicamente aquí tampoco importa si tienes o no __MigrationHistory, simplemente no se consulta).

Por defecto, la estrategia activa es CreateDatabaseIfNotExists (¡menos mal!). Esta estrategia es la única que nos garantiza que nuestra aplicación en producción no eliminará vilmente nuestra base de datos en cada ejecución o si el modelo cambia. Por el contrario, si el modelo no es compatible la inicialización fallará y nuestra aplicación quedará inaccesible. Como resolver este escenario lo veremos más adelante.

Respecto a la estrategia DropCreateDatabaseIfModelChanges, es normalmente utilizada en el entorno de desarrollo donde los datos son “prescindibles” y queremos agilidad a la hora de programar nuestra aplicación sin importar si ha habido o no cambios en el modelo.

Por último, con DropCreateDatabaseAlways estamos yendo un paso más allá y podría resultar útil si realizamos pruebas unitarias que tengan acceso a la base de datos y queremos asegurarnos de disponer siempre de una base de datos vacía en cada ejecución.

El cómo establecer un inicializador es muy sencillo:

Database.SetInitializer(new CreateDatabaseIfNotExists<TiendaContext>());

using (var context = new TiendaContext())

{

    // hacer algo...

}

Llegado el caso también podemos no establecer ningún inicializador. Esto puede resultarnos útil si estamos trabajando contra una base de datos existente y no queremos que Code First lleve a cabo ninguna estrategia de inicialización.

Database.SetInitializer<TiendaContext>(null);

Como era de suponer, podemos crear nuestra propia estrategia de inicialización personalizada para Code First. Para nuestro ejemplo, crearemos un inicializador con las siguientes características:

  • Si el modelo cambia, se recreará la base de datos.
  • También se recreará la base de datos si se encuentra un setting DropIsRequired con el valor true en nuestro Web.config.
  • Por otro lado, también debería ser posible ejecutar sentencias sql personalizadas después de haber creado la base de datos.

El código de inicializador, a continuación:

    public class DropCreateIfModelChangesOrDropIsRequired<TContext>

        : IDatabaseInitializer<TContext>

        where TContext : DbContext

    {

        public IEnumerable<string> Sentences { get; set; }

 

        public void InitializeDatabase(TContext context)

        {

            var created = false;

            if (!context.Database.Exists())

            {

                context.Database.Create();

                created = true;

            }

            else

            {

                var dropIsRequired = false;

                if (ConfigurationManager.AppSettings["DropIsRequired"] != null)

                {

                    var result = false;

                    if (bool.TryParse(ConfigurationManager.AppSettings["DropIsRequired"], out result))

                    {

                        dropIsRequired = result;

                    }

                }

                if (dropIsRequired || !context.Database.CompatibleWithModel(false))

                {

                    context.Database.Delete();

                    context.Database.Create();

                    created = true;

                }

            }

            if (created && Sentences != null)

            {

                foreach (var sentence in Sentences)

                {

                    context.Database.ExecuteSqlCommand(sentence);

                }

            }

        }

    }

 

Y el código necesario para la inicialización:

var initializer = new DropCreateIfModelChangesOrDropIsRequired<TiendaContext>();

initializer.Sentences = new List<string>()

    {

        "ALTER DATABASE CURRENT SET RECOVERY SIMPLE"

    };

Database.SetInitializer(initializer);

Otro punto interesante es saber cómo podemos agregar datos iniciales a nuestra base de datos. En los inicializadores que trae Code First de serie, podemos sobreescribir el método virtual Seed para centralizar la inicialización de datos después de que la estrategia de inicialización haya concluido. Por ejemplo, para CreateDatabaseIfNotExists

    public class CreateDatabaseIfNotExistsWithSeedData : CreateDatabaseIfNotExists<TiendaContext>

    {

        protected override void Seed(TiendaContext context)

        {

            context.Clientes.Add(new Cliente() { Nombre = "Sergio" });

        }

    }

Si hemos optado por crear un inicializador personalizado, no podremos sobreescribir este método (básicamente porque no existe) pero nada impide que lo implementemos y lo utilicemos igualmente. Por ejemplo, a nuestra anterior clase DropCreateIfModelChangesOrDropIsRequired le agregaremos el método Seed y una llamada a context.SaveChanges si la base de datos fue creada correctamente:

public class DropCreateIfModelChangesOrDropIsRequired<TContext>

    : IDatabaseInitializer<TContext>

    where TContext : DbContext

{

    public IEnumerable<string> Sentences { get; set; }

 

    public void InitializeDatabase(TContext context)

    {

        var created = false;

        if (!context.Database.Exists())

        {

            context.Database.Create();

            created = true;

        }

        else

        {

            var dropIsRequired = false;

            if (ConfigurationManager.AppSettings["DropIsRequired"] != null)

            {

                var result = false;

                if (bool.TryParse(ConfigurationManager.AppSettings["DropIsRequired"], out result))

                {

                    dropIsRequired = result;

                }

            }

            if (dropIsRequired || !context.Database.CompatibleWithModel(true))

            {

                context.Database.Delete();

                context.Database.Create();

                created = true;

            }

        }

        if (created)

        {

            if (Sentences != null)

            {

                foreach (var sentence in Sentences)

                {

                    context.Database.ExecuteSqlCommand(sentence);

                }

            }

            Seed(context);

            context.SaveChanges();

 

        }

    }

 

    protected virtual void Seed(TContext context)

    {

    }

}

Ahora crearemos una nueva clase que herede de DropCreateIfModelChangesOrDropIsRequired y que fijará el parámetro genérico al tipo TiendaContext. Además también aprovecharemos para incluir en el constructor por defecto nuestras sentencias personalizadas SQL:

    public class DropCreateIfModelChangesOrDropIsRequiredTiendaContext : DropCreateIfModelChangesOrDropIsRequired<TiendaContext>

    {

        protected override void Seed(TiendaContext context)

        {

            context.Clientes.Add(new Cliente() { Nombre = "Cliente por defecto" });

        }

        public DropCreateIfModelChangesOrDropIsRequiredTiendaContext()

        {

            Sentences = new List<string>() {

                        "ALTER DATABASE CURRENT SET RECOVERY SIMPLE" };

        }

    }

Ahora nuestra inicialización pasaría a ser la siguiente:

var initializer = new DropCreateIfModelChangesOrDropIsRequiredTiendaContext();

Database.SetInitializer(initializer);

Con esto habríamos logrado crear una nueva estrategia de inicialización personalizada de Code First y además implementar la inicialización de datos y sentencias personalizadas de SQL. ¡Bien! Ha costado pero hemos llegado.

Ya para terminar (que te prometo que yo también quiero terminar ya este post), mi última pregunta fue saber cómo podía cambiar de estrategia de inicialización según la configuración activa (algo parecido a lo que hicimos con las cadenas de conexión según estábamos en Debug o Release). Es decir, imagina que en Debug queremos el inicializador DropCreateIfModelChangesOrDropIsRequiredTiendaContext y en Release queremos CreateDatabaseIfNotExists (más que nada por no quedarnos con cara de bobos cuando publiquemos, haya un cambio en el modelo y nuestra base de datos desaparezca… ¡no quiero ni imaginarlo!). Pues bien, ya existe una solución muy elegante y que viene de serie con Code First y es la de elegir la estrategia de inicialización a través del fichero de configuración (de nuevo nuestro recurrente fichero Web.config).

Para seleccionar la estrategia adecuada a través del Web.config tendremos que agregar un setting con la clave DatabaseInitializerForType. Esto y junto a la transformación de ficheros de configuración, nos permitirán cambiar de estrategia sin tocar ni una sola línea de código.

Después y para nuestra configuración de Debug (fichero Web.config) escribiríamos lo siguiente donde value será el nombre nuestra clase o también el valor Disabled si queremos simplemente deshabilitar el inicializador (igual que hacíamos antes por código con null).

<appSettings>

  <add key="DatabaseInitializerForType Models.TiendaContext, Models"

      value="Models.DropCreateIfModelChangesOrDropIsRequiredTiendaContext, Models" />

</appSettings>

Por último, en el fichero Web.Release.config (el fichero de trasformación para Release) haríamos el siguiente cambio:

<appSettings>

  <add key="DatabaseInitializerForType Models.TiendaContext, Models"

    value="System.Data.Entity.CreateDatabaseIfNotExists`1 [[Models.TiendaContext, Models]], EntityFramework"

    xdt:Transform="SetAttributes"

    xdt:Locator="Match(key)"/>

</appSettings>

Cabe mencionar que si establecemos un inicializador tanto desde un fichero de configuración como desde código, prevalecerá el establecido en el fichero de configuración (incluyendo también como inicializador válido el valor Disabled).

Como apunte final, lo único que nos ha quedado por ver en relación al planteamiento inicial del post es cómo gestionar en un entorno de producción los cambios del modelo. Si piensas que con poner a nivel manualmente la base de datos para que coincida con el modelo es suficiente… pues será o no cierto en función de cómo se llame al método CompatibleWithModel. Recuerda que comparar el modelo con el esquema se realiza a través del contenido de la tabla __MigrationHistory, no leyendo la estructura de tablas de la base de datos. Es por ello que evolucionar la base de datos para que coincida con el modelo tiene que realizarse a través de Code First Migrations… pero eso será en otro post.

Un saludo!

martes, 19 de febrero de 2013

Compresión HTTP

Habilitar la compresión de ficheros en el servidor antes de ser enviados al explorador, puede suponer una notable mejora del ancho de banda utilizado por tu aplicación. La idea es sencilla, si el explorador es capaz de entender un fichero comprimido, el servidor lo comprimirá antes de enviarlo por la red y después el explorador lo descomprimirá cuando lo reciba.

Un navegador indica al servidor que soporta la compresión a través de la cabecera Accept-Encoding. En esta cabecera se listan los modos de compresión que acepta el navegador. Normalmente tendrá un valor similar a gzip,deflate. Es decir, el navegador aceptará contenido comprimido desde el servidor con gzip o con deflate.

En cuanto al servidor, deberá tener activada la compresión y cuando un cliente envíe la cabecera Accept-Encoding con un valor válido, comprimirá el fichero antes de servírselo al cliente.

El servidor (hablando siempre en términos de IIS) diferencia entre contenido estático y dinámico. Comprimir el contenido estático no incurre (o casi no incurre) en ningún gasto de procesador para el servidor, puesto que lo comprimirá la primera vez que algún cliente lo solicite y después lo servirá ya comprimido a siguientes clientes desde una cache temporal. Por otro lado, si hablamos de contenido dinámico, aquí sí se advierte que comprimirlo supondrá un esfuerzo extra para el servidor puesto que no podrá cachear el recurso y siempre que un cliente lo solicite tendrá que comprimirse de nuevo antes de ser enviado (fijarse que un recurso típico dinámico podría ser una página .aspx que para cada solicitud podría devolver resultados distintos, es por ello que no puede ser cacheada).

Para habilitar la compresión en IIS 6.0, podremos realizar parte del trabajo desde la interfaz gráfica (inetmgr) pero para otra parte tendremos que utilizar el comando adsutil.vbs disponible en la carpeta AdminScripts.

Desde la interfaz gráfica podremos habilitar o deshabilitar la compresión estática y dinámica, así como establecer el directorio donde se cacheará los ficheros estáticos y, opcionalmente, poner un límite de tamaño a este directorio. Para ello hay que abrir la opción “Properties” en el nodo “Web Sites”. Todo esto está muy bien explicado en el siguiente enlace: Enabling HTTP Compression (IIS 6.0).

Services

El momento en el que tendremos que utilizar adsutil.vbs es a la hora de configurar que extensiones de ficheros son considerados estáticos y cuales dinámicos. Customizing the File Types IIS Compresses (IIS 6.0). Aunque en el anterior enlace está todo muy bien explicado, personalmente he encontrado problemas con la sintaxis propuesta para agregar las extensiones. La clave está en que el separador de extensiones (diga lo diga el anterior enlace) es CR/LF y no un blanco como dice la MSDN. A continuación te dejo un par de enlaces que aportan mucha luz a posibles problemas que podrían surgir al habilitar la compresión:

Troubleshooting HTTP Compression in IIS 6.0

HTTP Compression and IIS 6.0

Por otro lado, si eres un valiente (y quizás también un temerario), podrías editar directamente la metabase de IIS 6.0 para realizar los cambios oportunos. La metabase está en C:\WINDOWS\system32\inetsrv\MetaBase.xml. Por ejemplo la sección dedicada a la compresión podría ser como la siguiente:

<IIsCompressionScheme    Location ="/LM/W3SVC/Filters/Compression/deflate"
        HcCompressionDll="%windir%\system32\inetsrv\gzip.dll"
        HcCreateFlags="0"
        HcDoDynamicCompression="TRUE"
        HcDoOnDemandCompression="TRUE"
        HcDoStaticCompression="TRUE"
        HcDynamicCompressionLevel="0"
        HcFileExtensions="png
            jpg
            htm
            html
            css
            js
            txt"
        HcOnDemandCompLevel="10"
        HcPriority="1"
        HcScriptFileExtensions="asp
            dll
            exe
            aspx
            png
            jpg
            js"
    >
</IIsCompressionScheme>
<IIsCompressionScheme    Location ="/LM/W3SVC/Filters/Compression/gzip"
        HcCompressionDll="%windir%\system32\inetsrv\gzip.dll"
        HcCreateFlags="1"
        HcDoDynamicCompression="TRUE"
        HcDoOnDemandCompression="TRUE"
        HcDoStaticCompression="TRUE"
        HcDynamicCompressionLevel="0"
        HcFileExtensions="png
            jpg
            htm
            html
            css
            js
            txt"
        HcOnDemandCompLevel="10"
        HcPriority="1"
        HcScriptFileExtensions="asp
            dll
            exe
            aspx
            png
            jpg
            js"
    >
</IIsCompressionScheme>



Fíjate que las extensiones están separadas por un retorno de carro y que se configuran gzip y deflate de forma separada. Además puedes observar que algunas extensiones como jpg y png están incluidas tanto como contenido estático como dinámico. Esto es porque podría pasar que ASP.NET procesara este tipo de ficheros en algún momento y entonces pasarían a ser considerados dinámicos en vez de estáticos.


Si damos el salto a IIS 7.x o superior, lo cierto es que todo resultará ser más sencillo y, sobre todo más configurable. La principal diferencia es que en IIS 6.0 hablábamos de extensiones de ficheros y en IIS 7.x o superior, hablamos de tipos mime. Además, ahora podremos configurar la compresión directamente desde el fichero web.config de nuestra aplicación, desde la sección system.WebServer:


<urlCompression doDynamicCompression="true" doStaticCompression="true" />


Si queremos ampliar información sobre este elemento o sobre que tipos mime serán comprimidos, podemos navegar a los siguientes enlaces:


URL Compression


HTTP Compression


Lo cierto es que a medida que dispongamos de nuevos tipos mime, tendremos que ir actualizando la lista para tenerlos en cuenta, un ejemplo en Enabling dynamic compression (gzip, deflate) for WCF Data Feeds, OData and other custom services in IIS7


Un saludo!

martes, 15 de enero de 2013

Eliminar un proyecto de equipo de Team Foundation Service

Si estas trabajando con Team Foundation Service, tarde o temprano querrás eliminar un Team Project y entonces caerás en la cuenta de que esta operación no está soportada desde el sitio web.

La solución es sencilla. hay que utilizar el comando tfsdeleteproject.exe que viene incluido con VS2012. En el siguiente post está muy bien explicado How To: Delete a Team Project from your Team Foundation Service collection, sin embargo (y con la segura previsión de que olvidaré guardar el favorito) te dejo en este post los pasos a seguir:

Lo primero, lógicamente, es localizar el fichero tfsdeleteproject.exe.
En mi caso, está en C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE.
En cualquier caso, si abrimos la herramienta de comandos de VS2012 estará disponible por la variable de entorno PATH.

Después, ejecutar el siguiente comando:
tfsdeleteproject /collection:https://<TuColección>.visualstudio.com/DefaultCollection <NombreProyectoEquipo>

image

Y ya está, un saludo!

jueves, 13 de diciembre de 2012

Caso real de internacionalización de una aplicación ASP.NET

Recientemente he tenido que desarrollar un módulo que trabaja con números de semana. El obtener el número de semana que corresponde a una fecha no es difícil. Sin embargo, obtener el primer y último día de un número de semana es algo más complicado. Además y a nivel de base de datos, trabajo con Sql Server y tengo la necesidad de calcular también números de semana desde T-SQL.

Para obtener el número de semana que corresponde a una fecha, podemos utilizar un método como el siguiente:

static DayOfWeek GetFirstDayOfWeek()

{

    return CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;

}

static int GetWeekOfYear(DateTime aDate)

{

    var firstDayOfWeek = GetFirstDayOfWeek();

    var weekOfYear = CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(

        aDate, CalendarWeekRule.FirstDay, firstDayOfWeek);

    return weekOfYear;

}

Lo más relevante de este código es:

  • Con GetFirstDayOfWeek no se asume que el lunes será el día de comienzo de una semana, sino que se consulta el valor establecido por la cultura actual del subproceso. La cultura actual podría o no coincidir con la configuración regional del equipo, imagina una aplicación web donde según una selección del idioma se establece la cultura del subproceso.
  • Con el valor CalendarWeekRule.FirstDay se establece como se quiere calcular las semanas.

Realmente el enumerado CalendarWeekRule tiene 3 posibles valores, pero el más intuitivo y sencillo de comprender es FirstDay (aunque quizás y según los requerimientos de cada negocio podría no ser el más adecuado). Para ver el detalle de los 3 tipos de cálculo disponibles http://msdn.microsoft.com/en-us/library/system.globalization.calendarweekrule(v=vs.95).aspx. Si quieres cambiar el valor predeterminado de la propiedad CalendarWeekRule del objeto CultureInfo (no confundir con el parámetro del método GetWeekOfYear), tendrás que crear una nueva cultura a partir de la actual, especificar como se calcularán las semanas y después asignársela al subproceso actual (aunque insisto esto no es necesario pero si trabajas por ejemplo con Time Period Library for NET podría ser aconsejable http://www.codeproject.com/Articles/168662/Time-Period-Library-for-NET)

static void CreateCultureInfo(CalendarWeekRule calendarWeekRule, DayOfWeek firstDayOfWeek)

{

    var newCultureInfo = new CultureInfo(CultureInfo.CurrentCulture.Name);

    newCultureInfo.DateTimeFormat.CalendarWeekRule = calendarWeekRule;

    newCultureInfo.DateTimeFormat.FirstDayOfWeek = firstDayOfWeek;

    Thread.CurrentThread.CurrentCulture = newCultureInfo;

}

Además hay otro motivo importante por el que se ha escogido el valor FirstDay y es que la función DATEPART de T-SQL parece que funciona con este valor de forma predeterminada. Lo cierto es que aunque no haya podido confirmarlo oficialmente de esto, los ejemplos me dicen que es así porque los números de semana T-SQL coinciden con los de .NET (la verdad es que no se por que valor del enumerado anterior se rige el cálculo de SQL ni si tiene que ver algo el idioma de la instalación o la configuración regional del equipo…). Sinceramente, espero no tener que utilizar DATEPART con wk en ningún script de T-SQL.

SELECT DATEPART(wk,'20121231')

SELECT DATEPART(wk,'20130101')

SELECT DATEPART(wk,'20130107')

 

 

.NET

T-SQL

31/12/2012

54

54

01/01/2013

1

1

07/01/2013

2

2

Algo a tener muy en cuenta cuando se trabaja en T-SQL y con la función DATEPART es el valor del día marcado como inicio de la semana y que además viene determinado por el valor del idioma de la conexión (fíjate que nada tiene que ver con la configuración regional del equipo ni la cultura del subproceso).

Con DBCC USEROPTIONS se puede consultar estos valores y ver como en us_english el primer día de la semana es el domingo.

language

datefirst

Español

1

us_english

7

Para establecer el día de inicio de una semana tenemos varias posibilidades:

Una vez ya disponemos de cierta sincronización entre el cálculo que realiza .NET y el cálculo que realiza T-SQL, nuestro siguiente método nos ayudará a calcular el día de inicio y el día de fin de un número de semana (el código está prestado pero “entendido” y levemente “modificado” desde http://stackoverflow.com/questions/11309715/get-the-dates-for-getweekofyear-in-c). Puedo asegurar que funciona con CalendarWeekRule.FirstDay pero con el resto de valores del enumerado no lo he probado. También lo he probado con FirstFourDayWeek y funciona igualmente.

static DateTime GetFirstDateOfWeek(DateTime aDate)

{

    var firstDateOfWeek =

        aDate.AddDays(-((aDate.DayOfWeek - GetFirstDayOfWeek() + 7) % 7));

    if (firstDateOfWeek.Year != aDate.Year)

        firstDateOfWeek = new DateTime(aDate.Year, 1, 1);

    return firstDateOfWeek;

}

static DateTime GetLastDateOfWeek(DateTime aDate)

{

    var lastDateOfWeek =

        aDate.AddDays(((GetFirstDayOfWeek() - 1) - aDate.DayOfWeek + 7) % 7);

    if (lastDateOfWeek.Year != aDate.Year)

        lastDateOfWeek = new DateTime(aDate.Year, 12, 31);

    return lastDateOfWeek;

}

Hasta aquí llegó la teoría del cálculo de números de semana en ASP.NET y T-SQL, pero ahora es necesario hacer un ejercicio de reflexión para sopesar los pros y contras de esta solución en una aplicación ASP.NET con los siguientes requerimientos:

  • Una sola instancia de aplicación (modelo Saas).
  • Una base de datos por cliente.
  • Cada cliente tiene una Culture fija que no puede cambiar y que es la nativa al país de origen del cliente.
  • La aplicación tiene que soportar la selección por parte del usuario de las UICulture español e inglés, con independencia de la Culture establecida para el cliente.

Imaginemos que tenemos los siguientes clientes en la aplicación:

 

UICulture

Culture

USA

en o es

en-US

España

en o es

es-ES

Portugal

en o es

pt-PT

La UICulture es seleccionable por el usuario porque es indiferente para la aplicación en que idioma quiere visualizar la interfaz. A grandes rasgos, la elección de una UICulture u otra se traduce casi exclusivamente en la carga de determinados ficheros de recursos .resx.

La Culture tiene que ir fija por cada cliente porque determina el formato de fechas, números… y también como se comportan los cálculos con fechas y semanas. Aquí está claro que el usuario no puede cambiar la Culture porque, por ejemplo, no puedes pasar de ver € a ver $ sin ningún tipo de conversión. Tampoco puedes pasar de ver día/mes/año a mes/día/año sin que el usuario se lleve las manos a la cabeza. En definitiva, si vives en USA siempre trabajarás con $ y tus fechas siempre serán mes/día/año… otra cosa distinta es que quieras practicar el español y puedes cambiar la UICulture cuando te plazca.

Lógicamente, el establecer siempre la Culture y UICulture para cada recurso de tu aplicación, también conlleva que no importa en que idioma esté instalado tu servidor, nunca serás dependiente en este aspecto.

En este punto alguien podría pensar que ocurrirá si el cliente que hoy es Culture en-US quiera cambiar mañana a Culture es-ES (por alguna decisión política que escapa a mi entendimiento). Pues bien, tenemos un problema y los señores de soporte (léase programadores) tendrían que convertir muchos datos antes de poder hacer este cambio, pero esta es otra historía…

Otra punto importante e íntimamente ligado con la internacionalización de aplicaciones, es el uso de las fechas UTC.

En España tenemos un ejemplo de diferencia horaria con las Islas Canarias. En USA es el caos, jamás se me ocurriría pedir la hora a alguien por la calle… Con este variopinto escenario, las decisiones que hemos tomado al respecto de las fechas en la aplicación son las siguientes:

  • Cualquier usuario de la aplicación tendrá asociado un uso horario, que además podrá cambiar en función de donde se encuentre físicamente (sería ya la repera que por geolocalización le pudiéramos ubicar o proponerle un cambio, todo llegará…)
  • Cada vez que se quiere obtener la fecha actual del sistema, se llamará a un método que calculará la fecha a partir de la fecha UTC actual del servidor y aplicará el uso horario del usuario. Finalmente, será la fecha calculada la que se grabe en el típico campo CreatedDate.

Para permitir al usuario cambiar su uso horario, podemos darle un desplegable con una lista de opciones obtenidas desde System.TimeZoneInfo.GetSystemTimeZones

clip_image001

Asumiendo que el usuario está en Madrid, grabaremos un campo asociado con el valor “Romance Standard Time” que es la propiedad Id de la clase TimeZoneInfo. Después y cuando queramos obtener la fecha actual del usuario (que no del servidor) ejecutaremos el siguiente método:

static DateTime GetCurrentDateTime(string timeZoneInfoId)

{

    var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneInfoId);

    return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, timeZoneInfo);

}

De esta forma siempre grabaremos en un campo del tipo CreatedDate la fecha actual del usuario según su uso horario y no según la hora del servidor.

Está claro que la internacionalización de aplicaciones es un mundo en sí mismo, pero por algún lado hay que empezar!

Un saludo!