Desmitificando CodeFirst(2/2) V 4.3

En el anterior post Desmitificando CodeFirst(1/2), me he centrado en la versión 4.2 de Entity Framework y el objetivo de este no es otro que analizar la versión 4.3 y ver realmente que hay en la tabla “dbo.__MigrationHistory”.

Siguiente con el modelo anterior lo primero que tenemos que hacer es activar Migration, pero como no es el objetivo de este post os paso un link para quien no lo conozca y quiera profundizar.

EF 4.3 Automatic Migrations Walkthrough.

El objetivo claro es demostrar dos cosas.

1. Lo que se guarda en la tabla no es un Hash sino un Xml, eso si con aderezo:).

2. Ver lo parco que es EF en la gestión de Exceptions teniendo a mano toda la información para mostrarnos algo más que lo que nos dice.

Vamos con lo primero. Para ello no necesitamos nada más que leer con Ado.Net la tabla “dbo.__MigrationHistory” y descomprimir el contenido de la columna Model.

   1: using (SqlConnection cn = new SqlConnection(@"Tu Conexion"))

   2:  {

   3:      cn.Open();

   4:  

   5:      SqlCommand cmd = new SqlCommand("select top 1 Model from dbo.__MigrationHistory order by CreatedOn desc", cn);

   6:      SqlDataReader rd = cmd.ExecuteReader();

   7:      rd.Read();

   8:      byte[] b = rd[0] as byte[];

   9:  

  10:      using (MemoryStream ms = new MemoryStream(b))

  11:      {

  12:          using (GZipStream stream2 = new GZipStream(ms, CompressionMode.Decompress))

  13:          {

  14:              var Edmx = XDocument.Load(stream2);

  15:              Console.WriteLine(Edmx.ToString());

  16:          }

  17:  

  18:      }    

  19:      

  20:  

  21:  

  22:  }

  23:  

  24:  Console.ReadLine();

Con lo cual si el XDocument lo guardasmos en disco con la extensión edmx y agregamos este al proyecto vemos que es lo que hay en la tabla Migrations.

Dibujo

Ya si nos queda claro que estamos utilizando Xml:)

Vamos a seguir con nuestro querido “InvalidOperationException”. Y concretamente de lo que me quejo es de EF no nos informe de algo más en esta exception, puesto que podría decirnos exactamente que es lo que cambia entre un modelo y otro.

Para ello seguimos con la operación del anterior post y cambiamos  la longitud de la propiedad Nombre de 50 a 100 y obtenemos la misma exceptión pero con un mensaje diferente.

Dibujo

Claro lo fácil sería ejecutar los siguiente comandos desde Pakage Manager Console

1. Enable-Migrations.

2. Add-Migration "Salvameporfavor"

3. Update-Database –Verbose.

Y todos felices, pero se puede dar el caso que no me interese modificar la base de datos sino simplemente saber el porque y en consecuencia actuar. Para ello vamos de nuevo con Reflectión y ver que hace internamente EF para mostrarnos esta exceptión. En definitiva lo que hace es comparar el modelo guardado con el modelo generado.

El código para obtener las diferencias es el siguiente y después de mostrarlo vendrás las criticas, eso si constructivas:)

   1: XDocument documentDataBase = null;

   2:  

   3: using (SqlConnection cn = new SqlConnection(@"Tu Cadena de Conexion"))

   4: {

   5:     cn.Open();

   6:  

   7:     SqlCommand cmd = new SqlCommand("select top 1 Model from dbo.__MigrationHistory order by CreatedOn desc", cn);

   8:     SqlDataReader rd = cmd.ExecuteReader();

   9:     rd.Read();

  10:     byte[] b = rd[0] as byte[];

  11:     using (MemoryStream ms = new MemoryStream(b))

  12:     {

  13:         using (GZipStream stream2 = new GZipStream(ms, CompressionMode.Decompress))

  14:         {

  15:             documentDataBase = XDocument.Load(stream2);

  16:         }

  17:  

  18:     }       

  19:  

  20:  

  21: }

  22:  

  23: XDocument documentModel = null;

  24: using (ContextCliente ct = new ContextCliente())

  25: {

  26:     using (MemoryStream stream = new MemoryStream())

  27:     {

  28:         XmlWriterSettings settings = new XmlWriterSettings();

  29:         settings.Indent = true;

  30:         using (XmlWriter writer = XmlWriter.Create(stream, settings))

  31:         {

  32:             EdmxWriter.WriteEdmx(ct, writer);

  33:  

  34:         }

  35:         stream.Position = 0L;

  36:         documentModel = XDocument.Load(stream);

  37:     }

  38:  

  39:  

  40: }

  41:  

  42:  

  43: Assembly a = typeof(DbContext).Assembly;

  44:  

  45: var tipo = a.GetTypes().Where(c => c.FullName == "System.Data.Entity.Migrations.Infrastructure.EdmModelDiffer").FirstOrDefault();

  46:  

  47: var ModelDiffer = Activator.CreateInstance(tipo);

  48:  

  49: var Metodo = ModelDiffer.GetType().GetMethod("Diff");

  50:  

  51: var resultado = Metodo.Invoke(ModelDiffer, new object[] { documentDataBase, documentModel, null });

Si miramos el código estamos llamando al metodo “Diff” de la clase “System.Data.Entity.Migrations.Infrastructure.EdmModelDiffer”. Como no, definida con el modificador de acceso “Internal” 🙁 y este nos devuelve un IEnumerable<MigrationOperation> y en nuestro caso una clase derivada de MigrationOperation con nombre “AlterColumnOperation”, me suena a la migration que queda pendiente de ejecutar en la bb.dd.

Y ahora yo digo en vez de preguntar y devolver bool en el método “ModelMatches” de la clase “InternalContext” por que no se contruye una exception en condiciones diciendo en que difieren ambos modelos, veis como la critica es constructiva:)

ModelMatches de la clase InternalContext".

   1: public virtual bool ModelMatches(XDocument model)

   2: {

   3:     string connectionString = null;

   4:     return !new EdmModelDiffer().Diff(model, this.Owner.GetModel(), connectionString).Any<MigrationOperation>();

   5: }

Pues nada esperemos que alguien piense en ciertas mejoras y que estás nos ayuden a dormir mas felices:)

Desmitificando CodeFirst(1/2)

El día 22 tuve la suerte con @MiguelEgea, de dar una charla en Málaga .NET User Group y una de las cosas que afirme es que la tabla del sistema que se genera con la versión 4.3 “dbo.__MigrationHistory” en su columna “Model” no guarda un Hash sino que realmente es una copia del modelo, es decir un emdx comprimido.

Inmediatamente @pablonete me pone cara de incrédulo y que mejor que demostrarle eso con un post:).

Independientemente de demostrar esto lo primero que quiero desmitificar es que Codefirst utiliza Xml y no poco, en resumen el mismo que las otras dos modalidades DataBaseFirst y ModelFirst.

Lo segundo es que casi todos los que trabajamos con CodeFisrt tarde o temprano recibimos una “InvalidOperationException” tal y como os muestro.

Dibujo

El porque es sencillo tu modelo no coincide con lo que en versión 4.2 tenemos guardado en la tabla “EdmMetadata”.

Y es aquí donde @pablonete si tiene razón en versión 4.2 si se guarda un “Hash” mientras que en 4.3 no.

Vamos a demostrarlo con la versión 4.2.

1. Definimos un modelo sencillo que es valido para el ejemplo.

   1: //Entidad

   2: public class Cliente

   3: {

   4:    public int Id { get; set; }

   5:    public string Nombre { get; set; }

   6: }

   7: //Configuracion de la entidad Cliente

   8: public class ClienteEntityConfiguration : EntityTypeConfiguration<Cliente>

   9: {

  10:    public ClienteEntityConfiguration()

  11:    {

  12:        this.ToTable("Clientes");

  13:        this.HasKey(c => c.Id);

  14:  

  15:        this.Property(c => c.Nombre).HasMaxLength(50).IsRequired();

  16:    }

  17: }

  18: //DbContext

  19: public class ContextCliente : DbContext

  20: {

  21:    protected override void OnModelCreating(DbModelBuilder modelBuilder)

  22:    {

  23:        modelBuilder.Configurations.Add(new ClienteEntityConfiguration());          

  24:  

  25:    }

  26: }

Sí creamos un cliente y posteriormente nos vamos a la bb.dd y ejecutamos “select * from EdmMetadata” el resultado de la consulta es el siguiente.

Dibujo

La forma sencilla de provocar “InvalidOperationException” es cambiar en la clase ClienteEntityConfiguration esta línea

this.Property(c => c.Nombre).HasMaxLength(50).IsRequired();

Por esta otra

this.Property(c => c.Nombre).HasMaxLength(100).IsRequired();

A partir de ese momento nuestro Hash se convierte en lo siguiente.

0F9CDC01973A8E83D521518166B9F8C37E8D533704219CF1F2DC02B83AEE0F77”

El cual podemos obtener con el siguiente código.

   1: using (ContextCliente ct = new ContextCliente())

   2: {                

   3:     var model = EdmMetadata.TryGetModelHash(ct);             

   4: }

A simple vista vemos que hay una diferencia y es por eso el motivo de recibir “InvalidOpertationException”.

Lógicamente se pueden definir estrategias que para mi son peores que recibir la exception.

1. Eliminar la bb.dd cada vez que se hace un cambio, con algo como lo siguiente.

   1: System.Data.Entity.Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ContextCliente>());

2. Eliminar de las conventions “IncludeMetadataConvention” que aún es peor solución puesto que tarde o temprano cantará:).

Como se puede observar en versión 4.2 bastantes cosas desde mi punto de vista mal pensadas, pero bueno eso es parte del pasado y lo mejor como siempre es ser positivos:)

Ahora vamos a demostrar como ese Hash se genera a partir de un Xml y de esa forma eliminar parte del mito “CodeFisrt no utiliza Xml”. Señores síiii:)

Vamos a dejar nuestro ContextCliente de la siguiente forma y utilizar un montón de Reflection para ver lo que se convierte a Hash.

   1: public class ContextCliente : DbContext

   2: {        

   3:     protected override void OnModelCreating(DbModelBuilder modelBuilder)

   4:     {

   5:        

   6:         //Agregamos ClienteEntityConfigurtarion 

   7:         // a modelBuilder.Configuration

   8:         modelBuilder.Configurations.Add(new ClienteEntityConfiguration());

   9:         

  10:         //Obtenemos un DbModel

  11:         var Model = modelBuilder.Build(this.Database.Connection);

  12:     

  13:         //Obtenemos una clase internal

  14:         //Llamada "DbDatabaseMetadataExtensions"

  15:         Type t = (from b in typeof(DbContext).Assembly.GetTypes()

  16:                  where b.Name == "DbDatabaseMetadataExtensions"

  17:                  select b).FirstOrDefault();

  18:     

  19:         //De esa clase el Metodo ToStoreItemCollection

  20:         var Method = (from b in t.GetMethods(BindingFlags.Static | BindingFlags.Public)

  21:                       where b.Name == "ToStoreItemCollection"

  22:                       select b).FirstOrDefault();

  23:     

  24:         //Obtenemos del DbModel la propiedad "DatabaseMapping"

  25:         var DataBaseMappingProperty = Model.GetType().GetProperty("DatabaseMapping", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

  26:     

  27:         var DataBaseMapping = DataBaseMappingProperty.GetValue(Model, null);

  28:         //Obtenemos de DataBaseMapping la propiedad Database

  29:         var DataBaseMetadaProperty = DataBaseMapping.GetType().GetProperty("Database",BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

  30:     

  31:         var DataBase = DataBaseMetadaProperty.GetValue(DataBaseMapping, null);

  32:     

  33:         Action<string> Action = (xml) =>

  34:             {

  35:                 Console.WriteLine(ComputeSha256Hash(xml));                    

  36:             };

  37:         //Invocamos al metodoToStoreItemCollection

  38:         Method.Invoke(DataBase, new object[] { DataBase, Action });

  39:         Console.ReadLine();          

  40:     

  41:     }

  42:     

  43:     static string ComputeSha256Hash(string input)

  44:     {

  45:         byte[] buffer = GetSha256HashAlgorithm().ComputeHash(Encoding.ASCII.GetBytes(input));

  46:         StringBuilder builder = new StringBuilder(buffer.Length * 2);

  47:         foreach (byte num in buffer)

  48:         {

  49:             builder.Append(num.ToString("X2", CultureInfo.InvariantCulture));

  50:         }

  51:         return builder.ToString();

  52:     }

  53:     

  54:     

  55:     

  56:     static SHA256 GetSha256HashAlgorithm()

  57:     {

  58:         try

  59:         {

  60:             return new SHA256CryptoServiceProvider();

  61:         }

  62:         catch (PlatformNotSupportedException)

  63:         {

  64:             return new SHA256Managed();

  65:         }

  66:     }

  67: }

Solo os invito a que pongáis un breakPoint dentro del Action<string>

Dibujo

Y veáis el valor del parametro “xml”. Bueno para los más gandulones os lo muestro yo:)

   1: <?xml version="1.0" encoding="utf-16"?>

   2: <Schema Namespace="CodeFirstDatabaseSchema" Provider="System.Data.SqlClient" ProviderManifestToken="2008" Alias="Self" xmlns="http://schemas.microsoft.com/ado/2009/02/edm/ssdl">

   3:   <EntityType Name="Cliente">

   4:     <Key>

   5:       <PropertyRef Name="Id" />

   6:     </Key>

   7:     <Property Name="Id" Type="int" StoreGeneratedPattern="Identity" Nullable="false" />

   8:     <Property Name="Nombre" Type="nvarchar" MaxLength="100" Nullable="false" />

   9:   </EntityType>

  10:   <EntityType Name="EdmMetadata">

  11:     <Key>

  12:       <PropertyRef Name="Id" />

  13:     </Key>

  14:     <Property Name="Id" Type="int" StoreGeneratedPattern="Identity" Nullable="false" />

  15:     <Property Name="ModelHash" Type="nvarchar(max)" Nullable="true" />

  16:   </EntityType>

  17:   <EntityContainer Name="CodeFirstDatabase">

  18:     <EntitySet Name="Cliente" EntityType="Self.Cliente" Schema="dbo" Table="Clientes" />

  19:     <EntitySet Name="EdmMetadata" EntityType="Self.EdmMetadata" Schema="dbo" Table="EdmMetadata" />

  20:   </EntityContainer>

  21: </Schema>

Convencidos que el Hash de la versión 4.2 de CodeFirst se genera a partir del xml del Modelo:).

Bueno publico y voy con el segundo en CodeFirst 4.3 no es Hash sino un edmx comprimido.

El respositorio genérico.Un Derrochador en épocas de crisis.

Durante muchos días,semanas,meses e incluso años he visto la siguiente definición de un repositorio.

1. Interface IRepository.

   1: public interface IRepository<T> where T:class

   2: {

   3:    T Get(int Id);

   4:    T Insert(T Entity);

   5:    void Update(T Entity);

   6:    void Delete(T Entity);

   7:    IEnumerable<T> GetAll();

   8:  

   9:    IUnitOfWork UnifOfWork { get;}

  10: }

2. Interface IUnitOfWork

   1: public interface IUnitOfWork

   2: {

   3:     void Commit();

   4:     void RollBack();

   5: }

3. Interface IContext que implementamos en  nuestro contexto de Entity Framework, en ella simplemente voy a definir un método que me devuelva un DbSet<T>

   1: public interface IContext:IUnitOfWork

   2: {

   3:     DbSet<TEntity> CreateSet<TEntity>() where TEntity : class;

   4: }    

El objetivo de este post no es demostrar que evidentemente estamos ligados fuertemente a EF, desde el momento que utilizamos DbSet<T>, sino analizar la interface IRepository.

Para ello voy a crear una clase base o Layer Supertype donde voy a implementar la interface IRepository

   1: public class Repository<T> : IRepository<T> where T:class

   2: {

   3:     readonly IContext _Context;

   4:     readonly IUnitOfWork _UnitOfWork;

   5:     public Repository(IContext Context)

   6:     {

   7:         if (Context == null)

   8:             throw new NullReferenceException("El contexto no puede ser nulo");

   9:  

  10:         _Context = Context;

  11:         _UnitOfWork = Context;

  12:     }

  13:     public T Get(int Id)

  14:     {

  15:         return GetSet().Find(Id);

  16:     }

  17:  

  18:     public T Insert(T Entity)

  19:     {

  20:         throw new NotImplementedException();

  21:     }

  22:  

  23:     public void Update(T Entity)

  24:     {

  25:         throw new NotImplementedException();

  26:     }

  27:  

  28:     public void Delete(T Entity)

  29:     {

  30:         throw new NotImplementedException();

  31:     }

  32:  

  33:     public IEnumerable<T> GetAll()

  34:     {

  35:         return GetSet().AsEnumerable();

  36:     }

  37:  

  38:     public IUnitOfWork UnifOfWork

  39:     {

  40:         get { return _UnitOfWork; }

  41:     }

  42:     private DbSet<T> GetSet()

  43:     {

  44:         return _Context.CreateSet<T>();

  45:     }

  46: }

Como podéis observar los métodos Insert,Update y Delete no están implementados, puesto que como dije anteriormente no es objetivo de este post y aparte tampoco me apetece:).

Vamos a realizar un análisis de esto.

1. Estamos ligados a EF sí y no más comentarios:).

2.La interface IRepository<T> expone métodos que con total seguridad  no necesitemos? Sí.

Un ejemplo sencillo la entidad X no se  puede modificar ni eliminar. Para ello os recomiendo esta lectura que ayer concretamente twitee.

https://twitter.com/_PedroHurtado/status/210311759872000000

3. Si vemos estas pegas y lo seguimos utilizando, porque no le damos una vuelta de tuerca. Para ello os voy a exponer el siguiente requisito y lógicamente no voy a escribir todas las entidades necesarias sino quiero que os lo imaginéis:).

Un Cliente tiene una forma de pago asignada por defecto, para que cuando cree una nueva factura se la asigne a esta. Con no otro objetivo que ayudar al usuario y este lógicamente podrá cambiarla por cada operación, entendido no?

Tal y como hemos creado nuestro repositorio no nos queda otra opción que leer todos los datos del cliente y no son tres campos en la vida real. Para obtener el Id y la Descripción de la forma de pago tenemos que leer todos los datos del cliente y además hacer otra lectura para recuperar el nombre de la forma de pago. Luego nos quejamos que EF es lento o somos nosotros los que lo hacemos lento.

Porque no exponemos nuestro método Get con la siguiente firma

TResult Get<TResult>(int Id,Expression<Func<T,TResult>> Select);

Lo mismo ahora podemos implementar nuestro Get de esta forma

   1: public TResult Get<TResult>(int Id, Expression<Func<T, TResult>> Select)

   2: {

   3:     return GetSet()

   4:             .Where(c => c.Id == Id)

   5:             .Select(Select)

   6:             .FirstOrDefault();

   7: }

y definir un repositorio para la clase Cliente que nos devuelva bien la Entidad Cliente o parte de ella, queréis verlo?

Bueno pues escribimos las clases necesarias para ello y veréis:)

   1: public class Entity

   2: {

   3:    public int Id { get; set; }

   4: }

   5: public class Cliente:Entity

   6: {       

   7:    public string Nombre { get; set; }

   8:    public FormaPago FormaPago { get; set; }

   9: }

  10: public class FormaPago:Entity

  11: {

  12:    public string Nombre { get; set; }

  13: }

  14: public interface IRepositoryCliente : IRepository<Cliente>

  15: {

  16:  

  17: }

  18: public class RepositoryCliente : Repository<Cliente>, IRepositoryCliente

  19: {

  20:    public RepositoryCliente(IContext Context)

  21:        : base(Context)

  22:    {

  23:    }

  24: }

  25:  

  26: public class Contexto : DbContext, IContext

  27: {

  28:  

  29:    public DbSet<TEntity> CreateSet<TEntity>() where TEntity : class

  30:    {

  31:        throw new NotImplementedException();

  32:    }

  33:  

  34:    public void Commit()

  35:    {

  36:        throw new NotImplementedException();

  37:    }

  38:  

  39:    public void RollBack()

  40:    {

  41:        throw new NotImplementedException();

  42:    }

  43: }

Mirad lo que puedo hacer, ooooo!!!

   1: public class Test

   2: {

   3:     public Test()

   4:     {

   5:         using (Contexto ct = new Contexto())

   6:         {

   7:             var idCliente =10;

   8:             var repositorio = new RepositoryCliente(ct);

   9:             //Devolver un cliente

  10:             var cliente = repositorio.Get(idCliente, c => c);

  11:             //Devolver partes de un cliente

  12:             var formapago = repositorio.Get(idCliente, c => new { Id = c.FormaPago.Id, Nombre = c.FormaPago.Nombre });

  13:             

  14:         }

  15:     }

  16: }

Pero lo que más me gusta es esto.

Dibujo

Pedro la verdad que eres un sacrílego, no se cuantos principios  has roto y cuantos antipatrones has implementado, además un método que devuelve un Tipo Anónimo.

Yo me contesto. Si lo que tu me digas, pero yo necesito el Id y el Nombre de la Forma de Pago no doscientos campos que tiene el Cliente. Anda derrochador,malgastoso no vas a tener ni un duro cuando seas mayor:).

Conclusiones.

Porque no nos acostumbramos a leer exclusivamente lo que necesitamos y nos dejamos de películas, creo que la vida nos irá mejor. Una última reflexión, ¿me habré cargado AutoMapper?. Otro malgastoso, o estoy en el camino:).