Entity Framework (VI): Webservices


Hasta ahora hemos visto el funcionamiento de LINQ y Entity Framework. La siguiente serie de artículos estarán orientados hacia los servicios web, por lo que haremos una pequeña introducción aplicando los conocimientos que hemos obtenido hasta el momento.

Lo primero que deberemos aclarar es el propio concepto de servicio web. Ya vimos en artículos anteriores de qué se tratan, cómo se crean y cómo se consumen estas pequeñas aplicaciones cuyo objetivo es el intercambio de información entre distintas plataformas y lenguajes de modo estándar.

Para crear un nuevo servicio web, crearemos un nuevo proyecto web de tipo ASP.NET Empty Web Application y le asociaremos un nuevo nombre. Los servicios web reciben ese nombre porque operan sobre el protocolo HTTP, así que este será nuestro punto de partida.

A continuación volveremos la vista atrás y, tal y como vimos en los artículos dedicados a Entity Framework, añadiremos un nuevo modelo de datos a nuestro proyecto web.

Una vez añadido, el asistente nos preguntará el origen de los datos. Le responderemos que nuestro deseo es generarlo a partir de la base de datos y pulsaremos Next >

Tal y como hicimos en ocasiones anteriores, seleccionaremos los objetos a modelar: tablas, vistas, procedimientos almacenados…

Si todo ha ido bien, nuestro modelo debería generarse con los elementos seleccionados.

Creando el Data Service

A partir del Framework 3.5, Microsoft encapsuló la gestión de los servicios web en un conjunto de bibliotecas denominadas Windows Communication Foundation. Nuestra intención es crear un servicio de datos (veremos los tipos de webservices en posteriores artículos), por lo que haremos click derecho sobre nuestro proyecto web y añadiremos un nuevo elemento Web > WCF Data Service.

Al finalizar la generación, veremos que se han creado dos elementos: un fichero con extensión .svc, que representa el servicio web en sí y un fichero .svc.cs que contendrá el code behind o comportamiento, es decir, el código fuente que definirá las acciones que realizará.

Si aún no hemos tocado nada, el cuerpo del fichero c# será similar al siguiente:


namespace EntityFrameworkService
{
    public class GestionPedidosDataService : DataService< /* TODO: put your data source class name here */ >
    {
        // This method is called only once to initialize service-wide policies.
        public static void InitializeService(DataServiceConfiguration config)
        {
            // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
            // Examples:
            // config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead);
            // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
        }
    }
}

Como podemos ver, existe una sección TODO dentro de los símbolos «<» y «>» que nos indica que introduzcamos la clase de nuestra fuente de datos. Ésta no será otra que la clase generada en el paso anterior y que dio lugar a nuestro Entity Data Model, es decir, el DbContext. Por lo tanto, añadimos el nombre de la clase para que el servicio sepa a qué conjunto de datos podrá tener acceso.


     public class GestionPedidosDataService : DataService<TestDbContext>

Configuración y permisos

A continuación es posible asignar permisos a las diferentes entidades presentes en nuestro modelo de datos. Si quisiéramos proporcionar todos los permisos (lectura, escritura, eliminación), seleccionaríamos All. Si se desea un permiso en concreto, se seleccionará a título individual a partir de la enumeración EntitySetRights.

Si, por el contrario (y que será con toda seguridad el escenario más común) deseamos asignar más de un permiso a una entidad, los permisos se concatenarán con el símbolo OR binario «|». Por ejemplo, la siguiente configuración asigna permisos de lectura a todas las entidades, y, además, permisos de inserción (WriteAppend) a las entidades Pedido y LineaPedido:


    public class GestionPedidosDataService : DataService<TestDbContext>
    {
        // This method is called only once to initialize service-wide policies.
        public static void InitializeService(DataServiceConfiguration config)
        {
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;

            config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
            config.SetEntitySetAccessRule("Pedido", EntitySetRights.AllRead | EntitySetRights.WriteAppend);
            config.SetEntitySetAccessRule("LineaPedido", EntitySetRights.AllRead | EntitySetRights.WriteAppend);
        }
    }

Finalizada la configuración inicial, podemos iniciar nuestro servicio web y echar un vistazo a lo que está ocurriendo dentro. Para ello, haremos click derecho sobre el fichero .svc y seleccionaremos la opción View in Browser.

Esto abrirá una ventana que mostrará un fichero XML con un aspecto parecido al siguiente:

Como podemos ver, además de la cabecera, nos encontramos con un nodo service que posee un nodo workspace que contiene un conjunto de nodos collection. Estos elementos, como podremos adivinar nada más verlos, se corresponden a los elementos que nuestro DbContext se encarga de mapear.

Uno de los nodos tiene un atributo denominado href cuyo valor es Cliente. Probemos, por lo tanto, a navegar a esta ruta relativa indicando la siguiente dirección en nuestro navegador (cambiando el puerto por el que nos asigne el navegador).

http://localhost:6040/GestionPedidosDataService.svc/Cliente

Es posible que realizar esta operación provoque que se muestre un mensaje como el siguiente:

Si es nuestro caso significará que el navegador tiene activado por defecto la configuración de feeds. Esto se debe a que un gran número de aplicaciones web permiten realizar una suscripción a través de la cual, mediante un servicio web en este formato, se pueden recuperar los titulares y cabeceras de los últimos contenidos (como en el recientemente fallecido Google Reader). Debido a esto, puede que el navegador «transforme» esa respuesta XML en algo más «legible» para el ser humano, pero que en nuestro caso concreto, nos está haciendo un flaco favor.

Desactivarlo en Internet Explorer no es difícil. Basta con acceder a las propiedades y seleccionar la pestaña Contenido. Una vez allí, pulsaremos el botón Configuración.

A continuación, nos aseguraremos de que la casilla Activar la vista de lectura de fuentes se encuentra desactivada y pulsaremos Aceptar.

Si recargamos la página, nuestra colección habrá cambiado significativamente, mostrando algo similar a esto:

Probemos algo más. Sabemos que Entity Framework gestiona de forma interna una clave primaria para cada entidad. También sabemos que el modelo objetual que expone Entity Framework integra colecciones de objetos con los que el objeto actual se encuentra relacionado. Por lo tanto, probemos a comprobar los pedidos asociados al cliente cuya clave es «3». Basta con algo como lo siguiente:

http://localhost:6040/GestionPedidosDataService.svc/Cliente(3)/Pedidos

Consultas REST

Por lo tanto, ¿estamos insinuando que es posible realizar un conjunto de operaciones de consulta sobre nuestro DbContext usando para ello un navegador web? Así es. En realidad lo importante no es el navegador, sino el protocolo (HTTP), pero obviamente, un navegador es capaz de realizar este tipo de peticiones. Acabamos de realizar nuestra primera comunicación REST (REpresentational State Transfer). No profundizaremos ahora en ello (lo haremos en posteriores artículos), pero baste decir que normalmente nos encontraremos con dos tipos de servicios web: SOAP y RESTful.

En cuanto a los tipos de consultas que podemos realizar mediante REST, aquí se muestran unos pocos ejemplos:

  • $value: recupera el valor solicitado, sin metadatos asociados (sin XML, en este caso).
    • /Cliente(3)/Nombre/$value

  • $count: devuelve el total de registros de una entidad.
    • /Cliente/$count

  • $filter=condición: permite realizar una consulta. Equivaldría a un where.
    • /Cliente?$filter=IdCliente eq 3

  • $orderby=criterio_de_ordenación: permite ordenar los resultados.
    • /Cliente?$orderby=IdCliente desc

  • $expand=listado_a_expandir: realiza la consulta de los objetos referenciados que se le especifiquen como parámetro
    • /Cliente?$expand=Pedidos

  • entidadHija/$links/entidadPadre: proporciona la URL en la que se encuentra el elemento referenciado.
    • /Pedido(1)/$links/Clientes

  • $top=n: recupera los primeros n elementos
    • /Cliente?$top=1

  • $skip=n: ignora los primeros n elementos. En conjunción con top, sirve para realizar paginaciones.
    • /Cliente?$top=1&$skip=1

Creando el cliente

Generar un servicios de datos conectado a un DbContext ha sido sencillo: Entity Framework y WCF jugando mano a mano y haciéndonos más fácil el acceso a los datos. Es hora de dar un paso más y, en lugar de trastear con el navegador, crear un pequeño programa que sea capaz de realizar operaciones más concretas y complejas con los datos obtenidos por el servicio web. Comenzaremos haciendo click derecho sobre nuestra solución y seleccionando la opción de añadir un nuevo proyecto.

En mi caso crearé una pequeña aplicación de consola.

Lo siguiente será añadir una referencia a nuestro servicio. WCF proporciona un servicio de descubrimiento que, a partir de la dirección de un servicio web, extrae toda la información disponible asociada a éste. Haremos click derecho sobre nuestro proyecto cliente y seleccionaremos la opción Add Service Reference…

En la caja de dirección, insertaremos la URI del servicio web, incluyendo la extensión svc. Una vez hecho esto, pulsaremos en Go, dejando que Visual Studio descubra qué es lo que se encuentra al otro lado. Finalmente, le daremos un nombre al namespace, que servirá para identificar los objetos que se encuentran al otro lado.

Con algo tan sencillo como esto habremos configurado nuestra aplicación para comunicarse con el servicio web. Lo siguiente que haremos será crear una referencia al DbContext remoto, para lo cual le tendremos que pasar la URI del servicio web.


            Uri serviceRoot = new Uri("http://localhost:6040/GestionPedidosDataService.svc");
            var dbContext = new GestionPedidosDataService.TestDbContext(serviceRoot);

Consulta

Realizar una consulta será similar a lo que hacemos en local: realizamos una consulta LINQ to Entities buscando el objeto a recuperar, teniendo en cuenta que nuestro DbContext es limitado. Es importante darse cuenta de que no disponemos de todas las operaciones que podemos realizar sobre un contexto local: nos tendremos que limitar a operaciones más simples como condiciones, ordenaciones y paginaciones. Deberemos olvidarnos de joins, funciones de agregación o similares.

Eso no significa que no podamos realizar esas operaciones, sino que tendrán un mayor coste. Siempre es posible obtener la totalidad de registros de una tabla, transformarlos en una lista y realizar las operaciones complejas en entorno local. Sin embargo, si este tipo de operaciones son necesarias, será aconsejable hacer uso de otros recursos como procedimientos almacenados para que nuestro rendimiento y escalabilidad no se vean comprometidos.

Nuestro primer paso será, por lo tanto, crear una consulta y generar cuatro nuevos objetos: un cliente, un pedido y dos líneas de pedido asociadas a dos productos distintos que, previamente, existirán en la base de datos. Por ejemplo, una línea de pedido contendrá tres lapiceros y la otra, siete bolígrafos.

Por lo tanto, realizaremos dos consultas para recuperar los productos Lapicero y Boligrafo e instanciaremos los objetos que queremos insertar. Para las líneas de producto, indicaremos el identificador del producto en su campo IdProducto.


            // Obtenemos referencias a los productos que queremos referenciar
            Producto lapicero = dbContext.Producto.Where(producto => producto.Descripcion.Equals("Lapicero")).First();
            Producto boligrafo = dbContext.Producto.Where(producto => producto.Descripcion.Equals("Boligrafo")).First();

            // Creamos un nuevo cliente
            var nuevoCliente = new Cliente()
            {
                Nombre = "Pedro Gonzalez Arnau",
                FechaNacimiento = new DateTime(1988, 2, 2)
            };

            // Creamos un nuevo pedido y dos nuevas lineas de pedido
            Pedido pedido = new Pedido()
            {
                FechaPedido = DateTime.Now
            };

            LineaPedido lineaPedidoLapiceros = new LineaPedido()
            {
                IdProducto = lapicero.IdProducto,
                Cantidad = 3
            };

            LineaPedido lineaPedidoBoligrafos = new LineaPedido()
            {
                IdProducto = boligrafo.IdProducto,
                Cantidad = 7
            };

Inserción

La inserción resulta un poco más compleja que su operación equivalente en Entity Framework cuando trabajamos en local. Para realizar la operación correctamente es necesario indicar las relaciones de forma explícita (no servirá únicamente con añadir los objetos y dejar que Entity Framework se encargue.

Por ello, añadiremos el cliente mediante el método AddToCliente y pasándole el objeto cliente como parámetro. La referencia al DbContext remoto incorporará por defecto un conjunto de métodos AdToXXXXX que simplificará la tarea de añadir un registro a una colección.

Sin embargo, en el caso de las entidades relacionadas no bastará con añadirlas a sus tablas correspondientes: habrá que indicar al servicio web que los objetos están relacionados. Para ello se utilizará el método AddRelatedObject, que recibirá tres parámetros:

  • Objeto padre (por ejemplo, nuevoCliente)
  • Cadena de texto con el nombre de la colección del objeto padre en el que se insertará el objeto relacionado (por ejemplo, Pedidos).
  • Objeto relacionado (por ejemplo, pedido).

Realizaremos el mismo proceso con el pedido y sus respectivas líneas. No será necesario hacerlo con las líneas de pedido y los productos, ya que los valores de sus claves primarias fueron indicados explícitamente en el campo IdProducto de cada una de las líneas de pedido, relacionando los objetos de forma interna.


            // Insertamos el nuevo cliente
            dbContext.AddToCliente(nuevoCliente);

            // Añadimos la relación entre cliente y pedido al contexto
            dbContext.AddRelatedObject(nuevoCliente, "Pedidos", pedido);

            // Añadimos las relaciones entre pedido y lineas de pedido al contexto
            dbContext.AddRelatedObject(pedido, "LineasPedido", lineaPedidoBoligrafos);
            dbContext.AddRelatedObject(pedido, "LineasPedido", lineaPedidoLapiceros);

El siguiente paso será invocar el método SaveChanges para comprometer los cambios. Además, echaremos un vistazo dentro del valor que el servicio devuelve como respuesta para comprobar qué es lo que ha ocurrido en el otro lado de la conexión.


            // Guardamos los cambios
            DataServiceResponse respuesta = dbContext.SaveChanges(SaveChangesOptions.Batch);

            // Mostramos los cambios
            foreach (ChangeOperationResponse cambio in respuesta)
            {
                EntityDescriptor descriptor = (EntityDescriptor)cambio.Descriptor;
                if (descriptor != null)
                {
                    if (descriptor.Entity.GetType().IsAssignableFrom(typeof(Cliente)))
                    {
                        Console.WriteLine(string.Format("Cliente: {0}\t{1}\t{2}",
                            ((Cliente)descriptor.Entity).IdCliente, ((Cliente)descriptor.Entity).Nombre, descriptor.State.ToString()));
                    }
                    else if (descriptor.Entity.GetType().IsAssignableFrom(typeof(Pedido)))
                    {
                        Console.WriteLine(string.Format("\tPedido: {0}\t{1}\t{2}",
                            ((Pedido)descriptor.Entity).IdPedido, ((Pedido)descriptor.Entity).FechaPedido.ToShortDateString(), descriptor.State.ToString()));
                    }
                    else if (descriptor.Entity.GetType().IsAssignableFrom(typeof(LineaPedido)))
                    {
                        Console.WriteLine(string.Format("\t\tLinea: {0}\t{1}\t{2}",
                            ((LineaPedido)descriptor.Entity).IdLineaPedido, ((LineaPedido)descriptor.Entity).Cantidad, descriptor.State.ToString()));
                    }

                }
            }

Esto nos devolverá la siguiente información. Como podemos observar, los campos de los identificadores, por ejemplo, ya habrán sido rellenados por Entity Framework.

A continuación mostraremos, mediante foreach anidados, una consulta de todos los clientes con ID = 61 (el que acabamos de insertar) junto a sus pedidos y líneas de pedido.


           var clientes = dbContext.Cliente.Where(cliente => cliente.IdCliente == 61);
           Console.WriteLine("\n\nTRAS LA INSERCION:");
            foreach (var cliente in clientes)
            {
                Console.WriteLine(string.Format("\tID: {0}\tNOMBRE: {1}",
                    cliente.IdCliente, cliente.Nombre));
                foreach (var p in cliente.Pedidos)
                {
                    Console.WriteLine(string.Format("\t\tID: {0}\tAÑO: {1}",
                        p.IdPedido, p.FechaPedido.Year));
                    foreach (var l in p.LineasPedido)
                    {
                        Console.WriteLine(string.Format("\t\t\tPRODUCTO: {0}\tCANTIDAD: {1}",
                            l.Productos.Descripcion, l.Cantidad));
                    }
                }
            }

Si ejecutamos este código veremos, asombrados, que únicamente se habrá recuperado el ID y el Nombre del cliente, pero no se mostrará nada de información acerca de pedidos o líneas de pedido. Esto se debe a que, por defecto, la petición de consulta es lazy, y habrá que indicar específicamente que se quiere recuperar la información de los listados asociados.

Para realizar esta operación, basta con indicar con el método LoadProperty el listado del objeto que se quiere recuperar, siendo el primer parámetro el objeto cuyo listado se quiere expandir y el segundo, una cadena de texto con el nombre del listado. Así, nuestro código tendría el siguiente aspecto:


           Console.WriteLine("\n\nTRAS LA INSERCION:");
            foreach (var cliente in clientes)
            {
                dbContext.LoadProperty(cliente, "Pedidos");
                Console.WriteLine(string.Format("\tID: {0}\tNOMBRE: {1}",
                    cliente.IdCliente, cliente.Nombre));
                foreach (var p in cliente.Pedidos)
                {
                    dbContext.LoadProperty(p, "LineasPedido");
                    Console.WriteLine(string.Format("\t\tID: {0}\tAÑO: {1}",
                        p.IdPedido, p.FechaPedido.Year));
                    foreach (var l in p.LineasPedido)
                    {
                        dbContext.LoadProperty(l, "Productos");
                        Console.WriteLine(string.Format("\t\t\tPRODUCTO: {0}\tCANTIDAD: {1}",
                            l.Productos.Descripcion, l.Cantidad));
                    }
                }
            }

Esto sí provocará la carga de los listados asociados a cada entidad, mostrando la información pertinente.

Modificación

La modificación es sencilla: basta recuperar el objeto deseado, invocar el método UpdateObject pasándole como parámetro la entidad que hemos modificado e invocar el método SaveChanges para que éstos se reflejen en el lado del servidor.


            // MODIFICACION

            dbContext = new GestionPedidosDataService.TestDbContext(serviceRoot);
            clientes = dbContext.Cliente.Where(cliente => cliente.IdCliente == 61);

            var clienteModificar = clientes.First();;
            clienteModificar.Nombre = "Pedro Javier Gonzalez Arnau";

            var lineaPedidoModificar = dbContext.LineaPedido.ToList().Last();
            lineaPedidoModificar.Cantidad = 59;

            dbContext.UpdateObject(clienteModificar);
            dbContext.UpdateObject(lineaPedidoModificar);
            dbContext.SaveChanges();

Esta operación provocará el siguiente resultado:

Eliminación

Por último, el proceso de eliminación, que será similar al de modificación, salvo que invocaremos el método DeleteObject en lugar de UpdateObject. El proceso de eliminación en cascada es parecido al visto en la sección correspondiente de Entity Framework, por lo que nuevamente, suele aconsejarse utilizar procedimientos almacenados para realizar este proceso.


            // ELIMINACION

            dbContext = new GestionPedidosDataService.TestDbContext(serviceRoot);
            clientes = dbContext.Cliente.Where(cliente => cliente.IdCliente == 61);

            var lineaPedidoEliminar = (from linea in dbContext.LineaPedido
                                    where linea.IdLineaPedido == lineaPedidoBoligrafos.IdLineaPedido
                                    select linea).Single();

            dbContext.DeleteObject(lineaPedidoEliminar);
            dbContext.SaveChanges();

Tras eliminar los bolígrafos, esta sería la salida por pantalla.

Y con esto, finalizamos la sección dedicada a Entity Framework y abrimos la puerta a Windows Communication Foundation. Mañana realizaré un pequeño recopilatorio con todos los artículos relacionados con LINQ y Entity Framework para que, a modo de tutorial, se aborden todos los artículos que sirven de introducción a estas tecnologías.

3 comentarios

  1. Hola,

    Creo que son los tutoriales sobre EF mejor redactados y más claros posibles que he visto, me han ayudado bastante a conocer ciertos aspectos y a utilizar esta herramienta mucho mejor.

    Muchísimas gracias por tu ayuda.

  2. Estaba siguiendo el TUTORIAL DE LINQ Y ENTITY FRAMEWORK y en esta ultima pagina veo que hay muchas imágenes caídas. Entre a otros tutoriales y veo que también ha pasado lo mismo.

Deja un comentario