Llamar a un servicio OData desde un cliente .NET (C#)

por Mike Wasson

Descargar el proyecto completado

En este tutorial se muestra cómo llamar a un servicio OData desde una aplicación cliente de C#.

Versiones de software usadas en el tutorial

En este tutorial, le guiaré por la creación de una aplicación cliente que llama a un servicio OData. El servicio OData expone las siguientes entidades:

  • Product
  • Supplier
  • ProductRating

Diagram showing the O Data service entities and a list of their properties, with connecting arrows to show how each relate or work together.

En los artículos siguientes se describe cómo implementar el servicio OData en la API web. (Sin embargo, no es necesario leerlos para comprender este tutorial).

Generación del proxy de servicio

El primer paso es generar un proxy de servicio. El proxy de servicio es una clase .NET que define métodos para acceder al servicio OData. El proxy traduce las llamadas de método a solicitudes HTTP.

Diagram showing the service proxy's H T T P request calls running back and forth from the application, through the service proxy, and to the O Data service.

Para empezar, abra el proyecto de servicio OData en Visual Studio. Presione CTRL+F5 para ejecutar el servicio localmente en IIS Express. Anote la dirección local, incluido el número de puerto que asigna Visual Studio. Necesitará esta dirección al crear el proxy.

A continuación, abra otra instancia de Visual Studio y cree un proyecto de aplicación de consola. La aplicación de consola será nuestra aplicación cliente de OData. (También puede agregar el proyecto a la misma solución que el servicio).

Nota:

Los pasos restantes hacen referencia al proyecto de consola.

En el Explorador de soluciones, haga clic con el botón derecho en Referencias y seleccione Agregar referencia de servicio.

Screenshot of the solution explorer window, showing the menu under 'references' in order to add a new service reference.

En el cuadro de diálogo Agregar referencia de servicio, escriba la dirección del servicio OData:

http://localhost:port/odata

donde puerto es el número de puerto.

Screenshot of the 'add service reference' window, which shows the port number in the U R L address field and a field to add a Name space.

En Espacio de nombres, escriba "ProductService". Esta opción define el espacio de nombres de la clase de proxy.

Haga clic en Ir. Visual Studio lee el documento de metadatos de OData para detectar las entidades en el servicio.

Screenshot of the 'add service reference' dialog box, highlighting the container service, to show the operations running in it.

Haga clic en Aceptar para agregar la clase de proxy al proyecto.

Screenshot of the solution explorer dialog box, showing the menu under the 'product service client' and highlighting the option for 'Product Service'.

Crear una instancia de la clase de proxy de servicio

Dentro del método Main, cree una nueva instancia de la clase de proxy, como se indica a continuación:

using System;
using System.Data.Services.Client;
using System.Linq;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            Uri uri = new Uri("http://localhost:1234/odata/");
            var container = new ProductService.Container(uri);

            // ...
        }
    }
}

De nuevo, use el número de puerto real en el que se ejecuta el servicio. Al implementar el servicio, usará el URI del servicio activo. No es necesario actualizar el proxy.

El código siguiente agrega un controlador de eventos que imprime los URI de solicitud en la ventana de la consola. Este paso no es necesario, pero es interesante ver los URI de cada consulta.

container.SendingRequest2 += (s, e) =>
{
    Console.WriteLine("{0} {1}", e.RequestMessage.Method, e.RequestMessage.Url);
};

Consulta del servicio

El código siguiente obtiene la lista de productos del servicio OData.

class Program
{
    static void DisplayProduct(ProductService.Product product)
    {
        Console.WriteLine("{0} {1} {2}", product.Name, product.Price, product.Category);
    }

    // Get an entire entity set.
    static void ListAllProducts(ProductService.Container container)
    {
        foreach (var p in container.Products)
        {
            DisplayProduct(p);
        } 
    }
  
    static void Main(string[] args)
    {
        Uri uri = new Uri("http://localhost:18285/odata/");
        var container = new ProductService.Container(uri);
        container.SendingRequest2 += (s, e) =>
        {
            Console.WriteLine("{0} {1}", e.RequestMessage.Method, e.RequestMessage.Url);
        };

        // Get the list of products
        ListAllProducts(container);
    }
}

Tenga en cuenta que no es necesario escribir ningún código para enviar la solicitud HTTP o analizar la respuesta. La clase de proxy lo hace automáticamente al enumerar la colección Container.Products en el bucle foreach.

Al ejecutar la aplicación, la salida debe ser similar a la siguiente:

GET http://localhost:60868/odata/Products
Hat 15.00   Apparel
Scarf   12.00   Apparel
Socks   5.00    Apparel
Yo-yo   4.95    Toys
Puzzle  8.00    Toys

Para obtener una entidad por id., use una cláusula where.

// Get a single entity.
static void ListProductById(ProductService.Container container, int id)
{
    var product = container.Products.Where(p => p.ID == id).SingleOrDefault();
    if (product != null)
    {
        DisplayProduct(product);
    }
}

En el resto de este tema, no mostraré toda la función Main, solo el código necesario para llamar al servicio.

Aplicar opciones de consulta

OData define las opciones de consulta que se pueden usar para filtrar, ordenar, paginar datos, etc. En el proxy de servicio, puede aplicar estas opciones mediante varias expresiones LINQ.

En esta sección, mostraré breves ejemplos. Para obtener más información, consulte el tema Consideraciones de LINQ (Servicios de datos de WCF) en MSDN.

Filtrado ($filter)

Para filtrar, use una cláusula where. El siguiente ejemplo filtra por categoría de producto.

// Use the $filter option.
static void ListProductsInCategory(ProductService.Container container, string category)
{
    var products =
        from p in container.Products
        where p.Category == category
        select p;
    foreach (var p in products)
    {
        DisplayProduct(p);
    }
}

Este código corresponde a la siguiente consulta de OData.

GET http://localhost/odata/Products()?$filter=Category eq 'apparel'

Observe que el proxy convierte la where cláusula en una expresión OData $filter.

Ordenación ($orderby)

Para ordenar, use una cláusula orderby. En el ejemplo siguiente se ordena por precio, de mayor a menor.

// Use the $orderby option
static void ListProductsSorted(ProductService.Container container)
{
    // Sort by price, highest to lowest.
    var products =
        from p in container.Products
        orderby p.Price descending
        select p;

    foreach (var p in products)
    {
        DisplayProduct(p);
    }
}

Esta es la solicitud de OData correspondiente.

GET http://localhost/odata/Products()?$orderby=Price desc

Paginación del lado cliente ($skip y $top)

En el caso de los conjuntos de entidades grandes, es posible que el cliente quiera limitar el número de resultados. Por ejemplo, un cliente podría mostrar 10 entradas a la vez. Esto se denomina paginación del lado cliente. (También hay paginación del lado servidor, donde el servidor limita el número de resultados). Para realizar la paginación del lado cliente, use los métodos LINQ Skip y Take. En el ejemplo siguiente se omiten los primeros 40 resultados y se toman los 10 siguientes.

// Use $skip and $top options.
static void ListProductsPaged(ProductService.Container container)
{
    var products =
        (from p in container.Products
          orderby p.Price descending
          select p).Skip(40).Take(10);

    foreach (var p in products)
    {
        DisplayProduct(p);
    }
}

Esta es la solicitud de OData correspondiente:

GET http://localhost/odata/Products()?$orderby=Price desc&$skip=40&$top=10

Seleccione ($select) y expanda ($expand)

Para incluir entidades relacionadas, use el método DataServiceQuery<t>.Expand. Por ejemplo, para incluir el Supplier para cada Product:

// Use the $expand option.
static void ListProductsAndSupplier(ProductService.Container container)
{
    var products = container.Products.Expand(p => p.Supplier);
    foreach (var p in products)
    {
        Console.WriteLine("{0}\t{1}\t{2}", p.Name, p.Price, p.Supplier.Name);
    }
}

Esta es la solicitud de OData correspondiente:

GET http://localhost/odata/Products()?$expand=Supplier

Para cambiar la forma de la respuesta, use la cláusula select de LINQ. En el ejemplo siguiente se obtiene solo el nombre de cada producto, sin otras propiedades.

// Use the $select option.
static void ListProductNames(ProductService.Container container)
{

    var products = from p in container.Products select new { Name = p.Name };
    foreach (var p in products)
    {
        Console.WriteLine(p.Name);
    }
}

Esta es la solicitud de OData correspondiente:

GET http://localhost/odata/Products()?$select=Name

Una cláusula select puede incluir entidades relacionadas. En ese caso, no llame a Expandir; el proxy incluye automáticamente la expansión en este caso. En el ejemplo siguiente se obtiene el nombre y el proveedor de cada producto.

// Use $expand and $select options
static void ListProductNameSupplier(ProductService.Container container)
{
    var products =
        from p in container.Products
        select new
        {
            Name = p.Name,
            Supplier = p.Supplier.Name
        };
    foreach (var p in products)
    {
        Console.WriteLine("{0}\t{1}", p.Name, p.Supplier);
    }
}

Esta es la solicitud de OData correspondiente. Observe que incluye la opción $expand.

GET http://localhost/odata/Products()?$expand=Supplier&$select=Name,Supplier/Name

Para obtener más información sobre $select y $expand, consulte Uso de $select, $expand y $value en Web API 2.

Agregue una nueva entidad

Para agregar una nueva entidad a un conjunto de entidades, llame a AddToEntitySet, donde EntitySet es el nombre del conjunto de entidades. Por ejemplo, AddToProducts agrega un nuevo Product al conjunto de entidades Products. Al generar el proxy, Servicios de datos de WCF crea automáticamente estos métodos AddTo fuertemente tipados.

// Add an entity.
static void AddProduct(ProductService.Container container, ProductService.Product product)
{
    container.AddToProducts(product);
    var serviceResponse = container.SaveChanges();
    foreach (var operationResponse in serviceResponse)
    {
        Console.WriteLine(operationResponse.StatusCode);
    }
}

Para agregar un vínculo entre dos entidades, use los métodos AddLink y SetLink. El código siguiente agrega un nuevo proveedor y un nuevo producto y, a continuación, crea vínculos entre ellos.

// Add entities with links.
static void AddProductWithSupplier(ProductService.Container container, 
    ProductService.Product product, ProductService.Supplier supplier)
{
    container.AddToSuppliers(supplier);
    container.AddToProducts(product);
    container.AddLink(supplier, "Products", product);
    container.SetLink(product, "Supplier", supplier);
    var serviceResponse = container.SaveChanges();
    foreach (var operationResponse in serviceResponse)
    {
        Console.WriteLine(operationResponse.StatusCode);
    }
}

Use AddLink cuando la propiedad de navegación sea una colección. En este ejemplo, vamos a agregar un producto a la colección Products en el proveedor.

Use SetLink cuando la propiedad de navegación sea una sola entidad. En este ejemplo, vamos a establecer la propiedad Supplier en el producto.

Actualizar o aplicar revisiones

Para actualizar una entidad, llame al método UpdateObject.

static void UpdatePrice(ProductService.Container container, int id, decimal price)
{
    var product = container.Products.Where(p => p.ID == id).SingleOrDefault();
    if (product != null)
    { 
        product.Price = price;
        container.UpdateObject(product);
        container.SaveChanges(SaveChangesOptions.PatchOnUpdate);
    }
}

La actualización se realiza cuando se llama a SaveChanges. De forma predeterminada, WCF envía una solicitud HTTP MERGE. La opción PatchOnUpdate indica a WCF que envíe un HTTP PATCH en su lugar.

Nota:

¿Por qué PATCH frente a MERGE? La especificación HTTP 1.1 original (RCF 2616) no definió ningún método HTTP con semántica de "actualización parcial". Para admitir actualizaciones parciales, la especificación de OData definió el método MERGE. En 2010, RFC 5789 definió el método PATCH para las actualizaciones parciales. Puede leer parte del historial de esta entrada de blog en el blog de Servicios de datos de WCF. En la actualidad, se prefiere PATCH a MERGE. El controlador OData creado por el scaffolding de API web admite ambos métodos.

Si desea reemplazar toda la entidad (semántica PUT), especifique la opción ReplaceOnUpdate. Esto hace que WCF envíe una solicitud HTTP PUT.

container.SaveChanges(SaveChangesOptions.ReplaceOnUpdate);

Eliminar una entidad

Para eliminar una entidad, llame a DeleteObject.

static void DeleteProduct(ProductService.Container container, int id)
{
    var product = container.Products.Where(p => p.ID == id).SingleOrDefault();
    if (product != null)
    {
        container.DeleteObject(product);
        container.SaveChanges();
    }
}

Invocar una acción de OData

En OData, las acciones son una manera de agregar comportamientos del lado servidor que no se definen fácilmente como operaciones CRUD en entidades.

Aunque el documento de metadatos de OData describe las acciones, la clase proxy no crea ningún método fuertemente tipado para ellas. Todavía puede invocar una acción OData mediante el método genérico Execute. Sin embargo, deberá conocer los tipos de datos de los parámetros y el valor devuelto.

Por ejemplo, la acción RateProduct toma el parámetro denominado "Rating" de tipo Int32 y devuelve un double. En el código siguiente se muestra cómo invocar esta acción.

int rating = 2;
Uri actionUri = new Uri(uri, "Products(5)/RateProduct");
var averageRating = container.Execute<double>(
    actionUri, "POST", true, new BodyOperationParameter("Rating", rating)).First();

Para obtener más información, consulteAcciones y operaciones del servicio de llamadas.

Una opción es extender la clase Container para proporcionar un método fuertemente tipado que invoca la acción:

namespace ProductServiceClient.ProductService
{
    public partial class Container
    {
        public double RateProduct(int productID, int rating)
        {
            Uri actionUri = new Uri(this.BaseUri,
                String.Format("Products({0})/RateProduct", productID)
                );

            return this.Execute<double>(actionUri, 
                "POST", true, new BodyOperationParameter("Rating", rating)).First();
        }
    }
}