EF y el transporte de grafos, primeros pasos

Sin lugar a dudas, la llegada de Entity Framework va a marcar un hito importante en los desarrollos sobre plataforma .NET; conceptualmente, nos ofrece una nueva visión del desarrollo, proporcionándonos un mecanismo sencillo para implementar Domain Driven Design (DDD), así como innumerables ventajas de productividad.


Dentro de EF, uno de los puntos más oscuros y de los que no se habla demasiado tiene que ver con el transporte de grafos de entidades en arquitecturas orientadas a servicios. A lo largo de este artículo, trataremos de centrarnos en los distintos mecanismos de serialización existentes y en mostrar cómo éstos pueden afectar al transporte de grafos de entidades creados mediante Entity Framework.


Identidad de objetos contra Equivalencia de objetos


Imagíne un sencillo escenario implementado con Entity Framework que contenga dos tablas, Clientes y Pedidos, como se muestra en la figura 1.



Figura 1


El mapeo por defecto de Entity Framework sobre este modelo relacional nos creará una propiedad de navegación por cada una de las entidades, de tal forma que cada cliente contendrá una propiedad que permitirá acceder a la colección de pedidos del cliente y cada pedido contendrá una propiedad que nos permitirá acceder al cliente asociado. Si sobre este modelo ejecutáramos la siguiente consulta:


List<Order> orders = (from order


in dbContext.Order.Include(«Customer»)


select order).ToList();


Podríamos encontrarnos con que muchos pedidos están asignados a un mismo cliente, y que por lo tanto la referencia obtenida mediante la propiedad de navegación de los pedidos debería ser la misma, ser idéntica, es decir el mismo objeto situado en una posición de memoria determinada y no una copia perfecta o un objeto equivalente. El elemento fundamental de EF que nos garantiza la consistencia de los grafos de entidades es ObjectStateManager; esta pieza nos permite además llevar el seguimiento de los cambios que sobre las entidades se producen.


ObjectStateManager basa su funcionamiento en unas estructuras fundamentales denominadas ObjectStateEntry, las cuales contienen información del estado de las entidades, su clave identificadora, un puntero hacia la entidad que representan y dos elementos de tipo IExtendedDataRecord para almacenar los valores originales y actuales con los que poder implementar los mecanismos de actualización y/o borrado de las mismas.


En la figura 2 podemos ver una imagen representativa de esta estructura.



Figura 2


En realidad, podríamos distinguir distintos tipos de ObjectStateEntry: los referidos a entidades, los referidos a relaciones y los ‘stubs’ . En el momento en el que una entidad es cargada dentro de un contexto de Entity Framework se crea una nueva estructura del tipo anterior, incluyendo la información del estado por defecto, Unchanged, la clave identificadora, un puntero hacia la entidad consultada y los valores de los elementos OriginalValues y CurrentValues.


En el caso en el que la entidad consultada contenga alguna propiedad de navegación, una asociación con la multiplicidad que fuere, además de la estructura ObjectStateEntry para la entidad se crea también una estructura de tipo relación. Estas estructuras de tipo relación no contienen valores para las propiedades Entity y EntityKey; solamente necesitan la propiedad CurrentValues para alojar un tipo AssociationType que relacione ambos roles de la asociación.


Si modificáramos la consulta anterior para obtener el primero de los pedidos encontrados en la base de datos tal y como se ve a continuación:


Order firstOrder = (from order


in dbContext.Order


select order).First();


La estructura que tendríamos dentro del ObjectStateManager para el caso anterior sería la mostrada en la figura 3.



Figura 3


En este caso, aunque solamente hayamos consultado una entidad simple, dentro del ObjectStateManager dispondremos de tres estructuras ObjectStateEntry: la primera de ellas para seguir la identidad y los cambios de la misma, una segunda estructura de tipo relación que contiene dentro de la propiedad CurrentValues, y una asociación que relaciona a las estructuras ObjectStateEntry involucradas. A pesar de que en la consulta anterior no hemos solicitado que se incluyera también el Cliente asociado al pedido, se ha creado una ObjectStateEntry que contiene el valor de la propiedad EntityKey, algo que conocemos por la clave externa de la tabla Order. Este último caso de ObjectStateEntry son conocidos como ‘stubs’, y nos permitirán guardar la consistencia de los grafos de relaciones de una forma sencilla. Si, posteriormente, lleváramos al contexto el cliente, aunque no fuera por medio de la relación, el servicio ObjectStateManager buscaría si contiene alguna estructura ObjectStateEntry con la propiedad EntityKey igual a la entidad que está incluyendo, y en caso de encontrarla asignaría los valores del Estado, CurrentValues, OriginalValues y crearía el puntero de la propiedad Entity hacia la entidad incluida (ver figura 4).


 


Según lo anteriormente dicho, si después de la consulta anterior ejecutáramos el siguiente trozo de código:


if (!firstOrder.CustomerReference.IsLoaded)


firstOrder.CustomerReference.Load();


La estructura ObjectStateEntry, de tipo Stub, pasaría a reflejar a una entidad cargada dentro del contexto, como vemos en la figura 5.


 



Figura 5


Para comprobar todo lo expuesto anteriormente, veamos cómo desde código podemos consultar las entradas incluidas dentro del ObjectStateManager, elemento al que tendremos acceso a través de un contexto de trabajo de Entity Framework. El siguiente trozo de código nos muestra cómo obtener las estructuras ObjectStateEntry de tipo entidad:


static void DumpEntityEntries(ObjectContext context)


{


//Obtain a ObjectStateManager reference


ObjectStateManager objectStateManager = context.ObjectStateManager;


//Get All unchanged entries


IEnumerable<ObjectStateEntry> entries = objectStateManager.GetObjectStateEntries(System.Data.EntityState.Unchanged);


//Get entity entries


IEnumerable<ObjectStateEntry> entityEntry = from e in entries


where !e.IsRelationship && e.Entity != null


select e;


//Write Entity Entry


Console.WriteLine(«**** Entity Entries ***** n»);


foreach (ObjectStateEntry entry in entityEntry)


Console.WriteLine(«t Entity :{0}»,entry.Entity);


Console.WriteLine();


}


Bastarían unas simples modificaciones en la consulta LINQ sobre el enumerador de las entradas para obtener las estructuras de tipo relación o de tipo stub y ver cómo está funcionando el mecanismo de consistencia e identidad de grafos después de cada consulta. Para nuestro caso, si mostráramos la información de las entradas en el ObjectStateManager después de obtener el primer pedido obtendríamos algo similar a lo siguiente:


**** Entity Entries ****


Entity :IdentityGraphModel.Order


**** Relations Entries *****


Role 0: Key ->idCustomer Value ->1


Role 1: Key ->idOrder Value ->1


**** Stub Entries *****


Keys:


Name:idCustomer Value:1


Si lanzáramos este mismo ejemplo después de cargar el cliente asociado al primer pedido, el resultado obtenido sería algo como:


**** Entity Entries *****


Entity :IdentityGraphModel.Order


Entity :IdentityGraphModel.Customer


**** Relations Entries *****


Role 0: Key ->idCustomer Value ->1


Role 1: Key ->idOrder Value ->1


**** Stub Entries *****


Llegados a este punto, ya conocemos cómo se forman los grafos de entidades de Entity Framework, y cómo efectivamente existe identidad y no equivalencia de objetos dentro de estos grafos. La pregunta ahora consiste en comprobar si esta misma consistencia e identidad de objetos se conserva al mover las entidades a través de servicios.


DataContractSerializer y XmlSerializer


Windows Communication Foundation pone a nuestra disposición dos serializadores para el transporte de mensajes entre los clientes y el servicio. DataContractSerializer es el serializador que se usa por defecto dentro de WCF para exponer los contratos de datos de los servicios, los cuales son decorados con los atributos DataContract y DataMember y que permiten compartir los esquemas y no los tipos, gracias a lo cual los servicios llegan a ser interoperables. Para los casos en los que necesitáramos tener un control más exhaustivo de la serialización de los tipos expuestos en un servicio, disponemos de XmlSerializer y los atributos que le acompañan, como XmlElement y XmlAttribute.


Una pregunta evidente que se nos ocurre cuando intentamos transportar grafos de entidades a través de servicios de WCF es saber si la consistencia de los mismos se mantiene. Intentaremos ver esto a través de unos sencillos ejemplos. Para ello, creamos dos contratos de datos como los que siguen:


[DataContract()]


public class Customer


{


[DataMember()]


public string FirstName { get; set; }


[DataMember()]


public string LastName { get; set; }


}


[DataContract()]


public class Order


{


[DataMember()]


public Customer Customer { get; set; }


[DataMember()]


public Decimal Total { get; set; }


}


Nuestro objetivo es comprobar si la serialización de varios objetos Order que apunten al mismo Customer tienen esa misma representación una vez deserializados, o si no se preserva la identidad de los objetos y ésta se cambia por una equivalencia. Para ello, creamos una lista genérica de objetos Order como sigue:


Customer customer = new Customer();


customer.FirstName = «Unai»;


customer.LastName = «Zorrilla Castro»;


Order order1 = new Order();


order1.Customer = customer;


order1.Total = 1000M;


Order order2 = new Order();


order2.Customer = customer;


order2.Total = 2000M;


List<Order> orders = new List<Order>() { order1, order2 };


Serializaremos esta lista genérica usando DataContractSerializer y comprobaremos si los objetos order1 y order2 apuntan a una misma referencia, para lo que nos apoyaremos en el siguiente método:


static void SerializeGraph(List<Order> orders)


{


//Create data contract serializer


DataContractSerializer serializer = new DataContractSerializer(typeof(Order),new Type[]{typeof(List<Order>)});


//Create memory stream


MemoryStream stream = new MemoryStream();


//Serialize object


serializer.WriteObject(stream,orders);


//Seek stream to initial position


stream.Seek(0,SeekOrigin.Begin);


//Deserialize graph


List<Order> deserializedGraph = (List<Order>)serializer.ReadObject(stream);


Console.WriteLine(«Is customer references equals:{0}»,Object.ReferenceEquals(deserializedGraph[0].Customer,deserializedGraph[1].Customer));


}


Si observamos el resultado, veremos que las referencias no son iguales; es decir, los objetos order1 y order2 dentro de la lista deserializada no apuntan a una misma referencia de objeto, aunque ambos contengan la misma información. Si echáramos un vistazo al mensaje transportado veríamos algo similar a lo siguiente:


<Order i:type=«ArrayOfOrder« xmlns=«http://schemas.datacontract.org/2004/07/GraphConsistency« xmlns:i=«http://www.w3.org/2001/XMLSchema-instance«>


    <Order>


        <Customer>


            <FirstName>Unai</FirstName>


            <LastName>Zorrilla Castro</LastName>


        </Customer>


        <Total>1000</Total>


    </Order>


    <Order>


        <Customer>


            <FirstName>Unai</FirstName>


            <LastName>Zorrilla Castro</LastName>


        </Customer>


        <Total>2000</Total>


    </Order>


</Order>


Lógicamente, si usted se fija, el resultado es normal, teniendo en cuenta la información del mensaje. Dentro de los elementos XML Customer tenemos presente la información a transmitir, pero en ambos elementos Order ésta se repite. Equivalencia de objetos, no hay forma alguna de que el serializador entienda que la referencia de los elementos debería ser la misma. DataContractSerializer nos ofrece una sobrecarga, en la que podemos especificar un valor booleano para notificar a WCF que deseamos preservar la identidad de los objetos. El siguiente trozo de código modifica la construcción del serializador para indicar que deseamos preservar la identidad de los objetos.


DataContractSerializer serializer = new DataContractSerializer(typeof(Order),new Type[]{typeof(List<Order>)},Int32.MaxValue,false,true,null);


Si realizamos el mismo ejemplo con esta nueva sobrecarga, el mensaje transferido variará considerablemente, como se puede apreciar a continuación:


<Order z:Id=«1« i:type=«ArrayOfOrder« z:Size=«2« xmlns=«http://schemas.datacontract.org/2004/07/GraphConsistency« xmlns:i=«http://www.w3.org/2001/XMLSchema-instance« xmlns:z=«http://schemas.microsoft.com/2003/10/Serialization/«>


    <Order z:Id=«2«>


        <Customer z:Id=«3«>


            <FirstName z:Id=«4«>Unai</FirstName>


            <LastName z:Id=«5«>Zorrilla Castro</LastName>


        </Customer>


        <Total>1000</Total>


    </Order>


    <Order z:Id=«6«>


        <Customer z:Ref=«3« i:nil=«true«/>


        <Total>2000</Total>


    </Order>


</Order>


Como puede apreciar, la presencia de los atributos Ref e Id permiten especificar que el contenido de un elemento está identificado dentro de otro elemento; en este caso, el objeto serializado order2 contiene dentro del elemento Customer una referencia, Ref con valor 3, el cual está identificado en el elemento Customer del objeto serializado order1. Ahora, ejecutando nuestro código veremos cómo obtenemos el comportamiento deseado y las referencias obtenidas del objeto Customer para ambos objetos Order son iguales. Por defecto, Windows Communication Foundation no permite de una forma inmediata especificar que el serializador DataContractSerializer use la preservación de las identidades de los objetos; de ahí que si usted va a transportar grafos de entidades construidos con Entity Framework debe prestar mucha atención en este aspecto. El primer paso para especificar cómo queremos construír nuestro serializador dentro de WCF será crear un nuevo DataContractSerializerOperationBehavior, clase incluída dentro de System.ServiceModel.Description, tal y como podemos ver a continuación:


public class DataContractWithPreserveIdentityOperationBehavior


:DataContractSerializerOperationBehavior


{


public DataContractWithPreserveIdentityOperationBehavior(OperationDescription operationDescription)


:base(operationDescription)


{


}


private static XmlObjectSerializer CreateDataContractSerializer(Type type, string name, string ns, IList<Type> knownTypes)


{


return CreateDataContractSerializer(type, name, ns, knownTypes);


}


#region Overrides


public override System.Runtime.Serialization.XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList<Type> knownTypes)


{


return CreateDataContractSerializer(type, name, ns, knownTypes);


}


public override System.Runtime.Serialization.XmlObjectSerializer CreateSerializer(Type type, System.Xml.XmlDictionaryString name, System.Xml.XmlDictionaryString ns, IList<Type> knownTypes)


{


return new DataContractSerializer(type, name, ns, knownTypes,


0x7FFF /*maxItemsInObjectGraph*/,


false/*ignoreExtensionDataObject*/,


true/*preserveObjectReferences*/,


null/*dataContractSurrogate*/);


}


#endregion


}


Una vez creado el comportamiento para nuestro serializador, crearemos un nuevo atributo de tipo OperationBehavior con el que decorar a los métodos de nuestro servicio que deseemos que preserven las identidades de los objetos:


public class PreserveIdentityAttribute : Attribute, IOperationBehavior


{


#region IOperationBehavior Members


public void AddBindingParameters(OperationDescription description, BindingParameterCollection parameters)


{


}


public void ApplyClientBehavior(OperationDescription description, System.ServiceModel.Dispatcher.ClientOperation proxy)


{


IOperationBehavior innerBehavior = new DataContractWithPreserveIdentityOperationBehavior(description);


innerBehavior.ApplyClientBehavior(description, proxy);


}


public void ApplyDispatchBehavior(OperationDescription description, System.ServiceModel.Dispatcher.DispatchOperation dispatch)


{


IOperationBehavior innerBehavior = new DataContractWithPreserveIdentityOperationBehavior(description);


innerBehavior.ApplyDispatchBehavior(description, dispatch);


}


public void Validate(OperationDescription description)


{


}


#endregion


}


Con esto, ya tendríamos nuestro trabajo terminado; bastaría con aplicar el atributo anterior a los métodos de servicio que deseáramos que preservaran la identidad de los objetos dentro de un grafo.


[ServiceContract(Name=«IOrderService»,Namespace=«http://www.plainconcepts.com/Oders»)]


public interface IOrderService


{


[OperationContract()]


List<Order> GetAllOrders();


}


[ServiceBehavior()]


public class OrderService


:IOrderService


{


#region IOrderService Members


[PreserveIdentity()]


public List<Order> GetAllOrders()


{


//TODO: Implement method here


return null;


}


#endregion


}


NOTA:


En estos momentos, las propiedades de navegación creadas mediante EDM están decoradas con los atributos XmlIgnoreAttribute y SoapIgnoreAttribute, lo cual impide que éstas participen en los procesos de serialización con el fin de evitar posibles ciclos, algo no permitido por XmlSerializer y DataContractSerializer. Si decidimos incluir propiedades en clases parciales para serializar las relaciones de las entidades, debemos tener en cuenta todo lo hablado hasta este momento con el fin de preservar la identidad de los objetos. Se prevee que para la versión final deEF, en el SP1 de Visual Studio 2008 se incluyan novedades en WCF que den soporte a la serialización de propiedades de navegación.


 


EDITADO:


La Beta del SP1 de Visual Studio 2008 incluye ya los cambios necesarios para dar soporte a la serialización de relaciones ( incluídos los ciclos ) y el soporte de conservación de la identidad de los objetos

Evento RAD Microsoft

Estos dos dias, Lunes y hoy Martes hemos estado impartiendo unas sesiones sobre frameworks en plataforma Microsoft, cuyos materiales estarán disponibles en el blog de Isabel ( DPE Microsoft Ibérica ). En concreto a mi me ha tocado dar la parte de LinqToSql y Entity Framework, como suele ser habitual no hay mucho tiempo como para explicarnos como quisiéramos por lo apretado de la agenda, aunque espero que los asistentes tengan las cosas un poco más claras…


Además de estas sesiones, también tuvimos NHibernate y Spring.Net por parte de Eduardo Quintas, compañero de Plain Concepts. Unity por parte de David Salgado y ASP.NET MVC por parte de Isabel entre otras muchas


 


Saludos


Unai

EDM y EF NO son el ORM de Microsoft

Seguramente mucha gente leyendo este post, está pensando, que es lo que está diciendo. De hecho uno mismo al principio de conocer los propósitos de Microsoft con EDM y EF también tenía la misma impresión. Llevo cada vez viendo más artículos y post que se centran precisamente en estos temas, y para agregar más confusión también alguno que otro que tiene que ver con Microsoft, y creo que es justo poner encima de la mesa ciertas cuestiones muy interesantes.


En primer lugar, decir que EDM y EF cubren un espacio similar al que cubren los ORM pero también cubren muchos otros escenarios, de hecho esta es una razón principal por la que comparar EDM y EF con un ORM como NHibernate puede dar lugar a confusiones en el sentido de ver ciertas carencias a EDM y EF. En realidad, puesto que no está centrado solamente en ORM hay ciertos grados de estos espacios que no están cubiertos por una herramienta tan madura como NHibernate, por poner un ejemplo de ORM. La filosofía de EDM y EF en el tiempo será mucho más amplia, es aquí donde estas herramientas nos ofrecerán un valor más grande. Una diferencia importante se puede observar con EDM, EDM no es solamente un diseñador de entidades para modelos relaciones unido a EF, EDM es un modelador conceptual común dentro de Microsoft, es fácil suponer que dentro de no mucho tiempo podremos ver ciertos productos, de Microsoft o no, haciendo uso de EDM sin necesidad de trabajar junto a Entity Framework. Por supuesto EDM nos ofrece muchas características que no son necesarias en un ORM como tal y también algunas restricciones que los espacios de ORM podrían llegar a necesitar pero EDM en esta versión no nos proporcionarán.


Las ventajas de ser algo más


Realmente, hasta ahora hemos dicho que EDM y EF son algo más, pero también hemos dicho que ‘por culpa’ de esto podríamos tener ciertas carencias. ¿ Que ganamos siendo algo más ? Puesto que tenemos un modelador como EDM capaz de cubrir más espacios que el de un ORM, podríamos imaginarnos que nuestra integración con otros productos se podría llegar a hacer a través de este modelador conceptual sin necesidad de conocer el store subyacente. ¿Reporting Services, Cubos, Sync Services…? No sé, suposiciones mías solamente.


A partir de esto mismo podríamos pensar que el nivel de integración con otros elementos del Framework como WCF, WF y ASP.NET lo que podría suponer una avance muy grande en productividad en nuestros desarrollos, algo que un ORM nunca tiene ya que siempre tiene que recurrir a herramientas externas para nada integradas. Con respecto a los nuevos productos podríamos fijarnos en DATA SERVICES ( ASTORIA ), que tendrá a EDM como sistema de metadatos y que se apoyara, probablemente en EF, para hacer que este framework no solamente sea de consulta.


Espero aclarar algo el panorama y que todos esperemos una visión más amplia de estos dos productos desde que sean realidad.


Saludos


Unai