EF y la consistencia del grafo

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

Cambios con el SP1 de VS2008 para Entity Framework

De primeras, seguro que hay muchos que aún no he visto y que tengo ganas de probar, pero de entrada hay unos cuantos que se ven muy claros:

  • Nuevo diseñador, algo más ligero que el anterior y más funcional
  • Por defecto los ficheros de Store, Conceptual y mapeo están embebidos en el ensamblado y no como output
  • ConcurrencyMode accesible y «funcional» desde el diseñador
  • Los marcadores Setter y Getter de las propiedades accesibles y modificables desde el diseñador
  • Nuevos elementos al menú contextual del Model Browser
  • La ventana de actualización del modelo es nueva, aunque le queda mucho para ser realmente funcional, sigue fallando en diversos toques para actualizar los cambios producidos en la base de datos, refrescar el modelo con una jerarquía terminada y alguna que otra, esta es una parte que está bastante floja…
  • Seguimos sin tener la posiblidad de hacer tipos complejos desde el diseñador, y algo que me mosqueo es que tampoco fuí capaz de hacerlo ‘a pelo’ ( aunque tampoco le dediqué demasiado tiempo, todo sea dicho de paso.
  • En cuanto al código el ObjectContext dispone de un nuevo método denominado CreateEntityKey mediante el cual podremos crear EntityKey pasando el nombre del EntitySet y el objeto al que le deseemos agregar este elemento.
  • La última, puede que sea una de las más importantes para la construcción de aplicaciones distribuídas y tiene que ver con la serialización de EntityCollection y EntityReferences, algo que tiene que ver con un post puesto hace algún tiempo con respecto a la serialización de grafos.

Seguro que me estoy olvidando de más, pero tal y como he comentado en cuanto vea más cosas interesantes las iré publicando…

 

Saludos

Unai Zorrilla

 

VS2008 en castellano y Entity Framework Tools CTP

Quienes tengan instalado Visual Studio 2008 en castellano (u otros idiomas diferentes del inglés, probablemente) y hayan intentado instalar la CTP de las herramientas de tiempo de diseño para ADO.NET Entity Framework (siguiendo los pasos que se recomiendan aquí), seguramente al intentar crear su primer modelo de entidades habrán pensado ¿y dónde está el asistente?

La respuesta está en que el asistente solo aparece cuando el entorno de desarrollo está utilizando el idioma inglés. La buena noticia es que si ha instalado VS 2008 en castellano, también tiene instalada «de serie» una versión en inglés. Basta con utilizar la opción del menú Herramientas | Opciones | Entorno | Configuración internacional, y seleccionar el idioma Inglés. Eso sí, el cambio no se hará efectivo hasta que cierre VS 2008 y lo vuelva a abrir.