miércoles, 12 de junio de 2013

WCF REST Service con JQuery



En esta nueva entrada veremos la forma de exponer un servicio WCF a través de un endpoint que soportará peticiones REST desde un cliente jquery.

¿Por qué un servicio WCF?, recordemos de la implementación del servicio está totalmente separada de la forma de comunicación, esto significa que podremos utilizar la misma implementación para exponer nuestra información a clientes de otras plataformas o a computadoras o servidores que se encuentren en la red interna de nuestro host.    Todo esto sin tener que estar generando nuevas implementaciones.

Es importante notar que utilizaremos la base de datos AdventureWorks, en caso de que no la tengas la puedes obtener de la siguiente dirección: http://msftdbprodsamples.codeplex.com/releases/view/93587

Abriremos el Visual Studio 2010 ejecutándolo como administrador (para evitar el uso de netsh para apartar el puerto) y comenzaremos por crear una solución en blanco llamada WCFJQuery, en esta solución vamos a agregar una biblioteca de clases y la llamaremos AWDB.

AWDB.

En este proyecto vamos a eliminar el archivo Class1.cs que trae por defecto y vamos a agregar un nuevo elemento de tipo ADO.NET Entity Data Model.    Si ya sabes cómo crear un modelo entonces sáltate hasta la creación del siguiente proyecto, la indicación es crear el modelo llamado AW con Production.ProductSubcategory y Production.Product con las entidades pluralizadas, de lo contrario sigue conmigo.

Al nuevo elemento de tipo ADO.NET Entity Data Model vamos a llamarlo AW.edmx, en el asistente elegimos la opción "Generate from database", vamos a crear la conexión hacia AdventureWorks utilizando autenticación de Windows dando clic en el botón "New connection", escribimos el nombre del servidor y elegimos la base de datos AdventureWorks de la lista desplegable, damos clic en "Test connection" y deberá salir un mensaje como el que se ve en la imagen.


Damos clic en Ok para cerrar el mensaje que dice que la conexión fue exitosa y damos clic en Ok para crear la conexión, el nombre de la cadena de conexión será AdventureWorksEntities.    Al dar clic en siguiente nos mostrará la lista de objetos utilizables y deberemos elegir las tablas Production.ProductSubcategory y Production.Product, elegimos la opción "Pluralize or singularize ..." y al modelo lo nombraremos AWModel; la configuración deberá quedar como se ve en la siguiente imagen:


Damos clic en finalizar y ya tenemos nuestro modelo terminado.

AWServices.

Vamos a agregar un proyecto de tipo biblioteca de clases y lo llamaremos AWServices, eliminamos el archivo Class1.cs que trae por defecto.    Agregamos referencia al proyecto AWDB, también tenemos que agregar referencia a System.Data.Entity y System.ServiceModel.Web.

También agregamos un elemento de tipo WCF Service llamado ProductsCatalog y una clase llamada Product que tenga las propiedades públicas Id (int), Name (string) y ListPrice (decimal) siendo todas de lectura-escritura, recuerda hacerla pública, por defecto al agregar una clase la declara como privada.

Ahora agregamos otra clase que se llame Subcategory que tendrá las propiedades públicas Id (int) y Name (string) siendo de lectura-escritura, recuerda hacerla pública.

En IProductsCatalog vamos a eliminar la operación DoWork que aparece por defecto y vamos a agregar una nueva que nos permitirá obtener los productos que pertenezcan a una subcategoría en particular con el siguiente código:

[OperationContract]
List<Product> GetProducts(int subcategoryId);

También vamos a agregar otra operación que nos devolverá la lista de subcategorías que estén registradas en la base de datos con el siguiente código:

[OperationContract]
List<Subcategory> GetSubcategories();

Ya que tenemos el contrato de nuestro servicio, ahora vamos a crear la implementación del mismo en el archivo ProductsCatalog.cs.

Eliminamos el procedimiento DoWork que trae por defecto, damos clic derecho en la interfaz que implementa y elegimos Implement Interface - Implement Interface, esto nos creará los stub necesarios para la implementación de las funciones que tiene la interfaz.

En la función GetSubcategories vamos a proyectar las subcategorías que están en la base de datos hacia una lista de objetos de tipo Subcategory con el siguiente código:

public List<Subcategory> GetSubcategories()
{
    WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
    using (AWDB.AdventureWorksEntities db = new AWDB.AdventureWorksEntities())
    {
        return db.ProductSubcategories.Select(sc => new Subcategory()
        {
            Id = sc.ProductSubcategoryID,
            Name = sc.Name
        }).ToList();
    }
}

En la función GetProducts buscaremos a los productos que pertenecen a la subcategoría cuyo identificador recibimos como parámetro y los proyectamos hacia una lista de objetos de tipo Product con el siguiente código:

public List<Product> GetProducts(int subcategoryId)
{
    WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
    using (AWDB.AdventureWorksEntities db = new AWDB.AdventureWorksEntities())
    {
        return db.Products.Where(p => p.ProductSubcategoryID == subcategoryId).Select(p => new Product()
        {
            Id = p.ProductID,
            Name = p.Name,
            ListPrice = p.ListPrice
        }).ToList();
    }
}

En ambas operaciones encontraremos una línea que agrega una cabecera Access-Control-Allow-Origin con un valor "*", esto es para que pueda ser utilizado desde cualquier dominio.

Hasta ahora nuestro servicio ya está listo para ser alojado y utilizado, pero debemos hacerle unas pequeñas modificaciones o ajustes a la implementación para que soporte REST.    Vamos a agregar el espacio de nombres System.ServiceModel.Web en la implementación del servicio y vamos a agregar el siguiente atributo a la función GetSubcategories:

[WebGet(ResponseFormat = WebMessageFormat.Json, UriTemplate = "Subcategories")]
public List<Subcategory> GetSubcategories()

Con esta configuración el mensaje que se devolverá a los clientes que realicen el request a la dirección Subcategories estará en formato JSON con lo que podremos utilizarlo de forma muy sencilla desde javascript.

También vamos a ajustar la función GetProducts para que reciba solicitudes utilizando REST:

[WebGet(ResponseFormat = WebMessageFormat.Json, UriTemplate = "Products?scid={subcategoryId}")]
public List<Product> GetProducts(int subcategoryId)

En esta configuración podemos notar que estamos agregando una variable por query string que se llamará scid y cuyo valor estará asignado al parámetro subcategoryId que solicita nuestra función.
Compila la solución y corrije cualquier detalle que aparezca en la lista de errores, si seguiste al pie de la letra lo que hemos hecho hasta ahora no deberás tener problema alguno.

AWHost.

Vamos a agregar un proyecto de tipo Aplicación de Consola y lo llamaremos AWHost, en este proyecto se estará alojando nuestro servicio AWServices.

Vamos a agregar referencia a System.ServiceModel y al proyecto AWServices.    Agregamos un nuevo elemento de tipo Application Configuration File y le dejaremos el nombre por defecto que mostrará: app.config, debemos abrir este archivo y pegar la cadena de conexión que tenemos en el proyecto AWDB en su archivo app.config, posteriormente registraremos nuestro servicio ProductsCatalog utilizando el siguiente código:

<system.serviceModel>
    <behaviors>
        <endpointBehaviors>
            <behavior name="HelpEnabled">
                <webHttp helpEnabled="true"/>
            </behavior>
        </endpointBehaviors>
    </behaviors>
    <services>
        <service name="AWServices.ProductsCatalog">
            <endpoint address="" binding="webHttpBinding" contract="AWServices.IProductsCatalog" behaviorConfiguration="HelpEnabled"></endpoint>
            <host>
                <baseAddresses>
                    <add baseAddress="http://JorgeToriz-PC:8001/AWServices"/>
                </baseAddresses>
            </host>
        </service>
    </services>
</system.serviceModel>

Deberás modificar el atributo baseAddress para que haga referencia al nombre de tu equipo, en el código que estoy poniendo hace referencia a mi equipo.

El behavior HelpEnabled permitirá que la dirección http://JorgeToriz-PC:8001/AWServices/help exponga todos los servicios que están a la disposición, en esta descripción aparecerá la forma en que deben llamarse y también los resultados que devolverán las funciones.

En el proyecto necesitamos referencia a System.ServiceModel.Web para poder alojar este servicio REST, para poder hacerlo necesitamos que el proyecto utilice el .NET Framework 4 y no el .NET Framework 4 Client Profile.    Para cambiar esta configuración abriremos las propiedades del proyecto y en el tab Application buscaremos la opción "Target Framework", de la lista desplegable deberemos elegir ".NET Framework 4", el IDE nos mostrará un mensaje de aviso, vamos a darle que "Sí" para que realice los cambios necesarios.

Ahora sí, vamos a agregar referencia a System.ServiceModel.Web y vamos a abrir nuestro archivo Program.cs, en él agregaremos el espacio de nombres System.ServiceModel.Web y escribiremos el siguiente código para poner disponible nuestro servicio:

static void Main(string[] args)
{
    WebServiceHost host = new WebServiceHost(typeof(AWServices.ProductsCatalog));
    host.Open();
    Console.WriteLine("El servicio ya está en ejecución, presiona cualquier tecla para detenerlo");
    Console.ReadKey();
    host.Close();
}

Al ejecutar el proyecto (sin debug) nos deberá mostrar una pantalla como esta:


Si visitamos la URL http://JorgeToriz-PC:8001/AWServices/help (cambiando JorgeToriz-PC por el nombre de tu equipo) deberá mostrar el navegador nuestra lista funciones como en la siguiente imagen:


Si damos clic en Products o Subcategories nos mostará la información que devuelve cada una de las URL.

AWClient.

Vamos a agregar un sitio web vacío a nuestra solución al que llamaremos AWClient, en ese proyecto crearemos una carpeta llamada js donde meteremos el script de jquery, si no lo tienes puedes descargar jquery de esta liga (http://code.jquery.com/jquery-1.10.1.min.js).

Dentro de nuestro sitio web vamos a agregar una página HTML a la que llamaremos ProductsCatalog, enlla agregaremos referencia a jquery y también agregaremos el siguiente código que hace llamadas utilizando REST para poder obtener el catálogo de productos:

<table>
    <tr>
        <td>Subcategories</td>
        <td>
            <select id="ddlSubcategories" onchange="return loadProducts()"></select>
            <script language="javascript" type="text/javascript">
                function loadProducts() {
                    $.ajax({
                        url: 'http://JorgeToriz-PC:8001/AWServices/Products?scid=' + $('#ddlSubcategories').val(),
                        type: 'GET',
                        success: loadProducts_success
                    });
                    return false;
                }
                function loadProducts_success(d) {
                    var products = '<table border="1"><tr><td>Id</td><td>Name</td><td>List price</td></tr>';
                    for (var i = 0; i < d.length; i++) {
                        products += '<tr>';
                        products += '<td>' + d[i].Id + '</td>';
                        products += '<td>' + d[i].Name + '</td>';
                        products += '<td>' + d[i].ListPrice + '</td>';
                        products += '</tr>';
                    }
                    products += '</table';
                    $('#divProducts').html(products);
                }
                function loadSubcategories() {
                    $.ajax({
                        url: 'http://JorgeToriz-PC:8001/AWServices/Subcategories',
                        type: 'GET',
                        success: loadSubcategories_success
                    });
                }
                function loadSubcategories_success(d) {
                    var subcategories = $('#ddlSubcategories');
                    for (var i = 0; i < d.length; i++) {
                        $('<option value="' + d[i].Id + '">' + d[i].Name + '</option>').appendTo(subcategories);
                    }
                    loadProducts();
                    subcategories.focus();
                }
                $(document).ready(loadSubcategories);
            </script>
        </td>
    </tr>
</table>
<div id="divProducts"></div>

Resumen.

Como podemos ver no es tan complicado el exponer un servicio WCF para ser utilizado como REST desde javascript, un punto fino es permitir las llamadas desde otros dominios utilizando la cabecera Access-Control-Allow-Origin en el WebOperationContext actual, mientras que el otro punto importante es elegir bien la estructura de las direcciones para REST y configurarlas en la implementación del servicio utilizando el atributo WebGet.

Espero te haya resultado de utilidad esta entrada.

2 comentarios:

  1. Hola Jorge,
    en el estilo de peticiones REST la uri:

    http://JorgeToriz-PC:8001/AWServices/Products?scid=1234

    se escribiria como

    http://JorgeToriz-PC:8001/AWServices/Products/1234

    just my 50 cents.

    Saludos,

    ResponderEliminar
    Respuestas
    1. Hola Max, muchas gracias por el comentario.

      He estado buscando algún documento que exponga alguna reglamentación para las URI de REST, pero no he podido encontrar algún estándar, hasta ahora han sido puramente acuerdos generalizados, y de hecho la forma que tú me comentas es la más aceptada.

      ¡Saludos!

      Eliminar