Una sinfonía en C#

Un pequeño aporte a la comunidad de habla hispana.

ASP.NET Web API, o cómo crear servicios REST sin recurrir a WCF?

ASP.NET Web API es una nueva capacidad agregada a ASP.NET MVC desde el developer preview de ASP.NET MVC 4, la idea es proveer un mecanismo pensado desde el principio para cubrir todas las interacciones no-humanas de nuestra aplicación, esto es principalmente llamadas Ajax.

Comenzando a usar la Web API

Una vez instalada la developer preview de MVC 4 tenemos el template de Web API disponible en Visual Studio 2011.

Abrimos Visual Studio y seleccionamos el template de MVC 4

image

 

Y ahora viene la parte diferentes, nos aparecen otras opciones desde la cuales podemos crear un proyecto Web API

image

Perfecto, una vez hecho esto tenemos la estructura del proyecto creada, si miramos lo que genera el template por defecto vemos que existen dos controladores:

  • HomeController
  • ValuesController

image

HomeController es un controlador tal cual lo conocemos

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace webApiTest.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
    }
}

Hasta ahora nada raro, sin embargo si vemos el código del ValuesController nos encontramos con algunas novedades

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http;

namespace webApiTest.Controllers
{
    public class ValuesController : ApiController
    {
        // GET /api/values
        public IEnumerable Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET /api/values/5
        public string Get(int id)
        {
            return "value";
        }

        // POST /api/values
        public void Post(string value)
        {
        }

        // PUT /api/values/5
        public void Put(int id, string value)
        {
        }

        // DELETE /api/values/5
        public void Delete(int id)
        {
        }
    }
}

Vemos dos cosas que nos llaman la atención

  • Hereda de ApiController en lugar de Controller
  • Las acciones retornan void o string el lugar de ActionResult

Otra cosa no menos es que las acciones tiene nombres REST-friendly como:

  • Get
  • Post
  • Put
  • Delete

Vamos a ver qué dice el global.asax sobre las rutas

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace webApiTest
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class WebApiApplication : System.Web.HttpApplication
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);

            BundleTable.Bundles.RegisterTemplateBundles();
        }
    }
}

Además de la clásica ruta Controlador/Acción, vemos una ruta (antes de ésta) que dice:

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

justamente en MapHttpRoute está la magia, pero qué es lo qué nos permite hacer

  • Consultas tipo REST, relacionando los nombres de las acciones con los verbos HTTP
  • Negociar, tipos de retorno, por ejemplo a través de headers HTTP nos devuelve XML o JSON

Vamos a modificar un poco el código por defecto para ver si esto es verdad

using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace webApiTest.Controllers
{
    public class ValuesController : ApiController
    {
        private static Dictionary values;

        public ValuesController()
        {
            if (values == null)
            {
                values = new Dictionary();
                values.Add(1, "valor1");
                values.Add(2, "valor2");
            }
        }

        public IDictionary Get()
        {
            return values;
        }

        public string Get(int id)
        {
            return values.First(v => v.Key == id).Value;
        }

        public void Post(string value)
        {
            var newId = ((int)values.Max(i=>i.Key)+1);
            this.Put(newId, value);
        }

        public void Put(int id, string value)
        {
            this.Delete(id);
            values.Add(id, value);
        }

        public void Delete(int id)
        {
            values.Remove(id);
        }
    }
}

Perfecto modificamos un poco para tener todos los verbos REST implementados y que funcione como debe, ahora para probarlo vamos al Fiddler, primer prueba

GET

En la solapa de “Compose” creamos un request utilizando GET a la Url configurada en global.asax

image

HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Sun, 19 Feb 2012 14:27:08 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Connection: Close
Content-Length: 89

[{"Key":1,"Value":"valor1"},{"Key":3,"Value":"valor3"},{"Key":4,"Value":"valor por put"}]

Nos responde como esperamos, en formato JSON, esto es porque pusimos (a propósito) el header Content-Type:application/json, podemos “negociar” que nos devuelve XML sin mucho esfuerzo, cambiando simplemente este header

Negociando el tipo de respuesta

Cambiamos en nuestro request el Content-Type y voilá

HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Sun, 19 Feb 2012 16:23:05 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: text/xml; charset=utf-8
Connection: Close
Content-Length: 262

<?xml version="1.0" encoding="utf-8"?><....

Mapeo de verbos automático

Bien, en nuestro código cambiamos las acciones para que funcionen como si fuera un ABM, asociamos cada acción de controlador con alta, baja, modificación, consulta..veámos

Consulta con GET

GET http://localhost:51757/api/values HTTP/1.1
Host: localhost:51757
Content-Type: application/json

Respuesta

HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Sun, 19 Feb 2012 16:43:59 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Connection: Close
Content-Length: 55

[{"Key":1,"Value":"valor1"},{"Key":2,"Value":"valor2"}]

Agregar con POST

POST http://localhost:51757/api/values HTTP/1.1
Host: localhost:51757
Content-Type: application/json
Content-Length: 44

{"id":"4","value":"valor agregado con POST"}

Y volvemos a consultar

HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Sun, 19 Feb 2012 18:17:42 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Connection: Close
Content-Length: 99

[{"Key":1,"Value":"valor1"},{"Key":2,"Value":"valor2"},{"Key":3,"Value":"valor agregado con POST"}]

Modificación con PUT

PUT http://localhost:51757/api/values HTTP/1.1
Host: localhost:51757
Content-Type: application/json
Content-Length: 52

{"id":"2","value":"el elemeto 2 modificado con PUT"}

Volvemos a consultar

HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Sun, 19 Feb 2012 18:20:44 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Connection: Close
Content-Length: 124

[{"Key":1,"Value":"valor1"},{"Key":2,"Value":"el elemeto 2 modificado con PUT"},{"Key":3,"Value":"valor agregado con POST"}]

y por último borrar con DELETE

DELETE http://localhost:51757/api/values/1 HTTP/1.1
Host: localhost:51757
Content-Type: application/json

y el resultado

HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Sun, 19 Feb 2012 19:42:16 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Connection: Close
Content-Length: 97

[{"Key":2,"Value":"el elemeto 2 modificado con PUT"},{"Key":3,"Value":"valor agregado con POST"}]

Notemos que en el caso de DELETE al no soportar enviar contenido en el body se utiliza la URL para identificar el recurso, como se declaró en la ruta del global.asax.

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

En resumen, ASP.NET Web API nos brinda un mecanismo para las llamas que no tiene un resultado visible para el humano, y nos evitar poner en nuestros controladores estos métodos o hacer un servicio WCF con WebHttpBinding, además está pensada para trabajar como REST.

La próxima vez vamos a ver cómo integrar esto con jQuery, hasta entonces.

Comments (4) -

  • Rodrigo Juarez

    2/23/2012 6:54:27 AM | Reply

    Justo en estos momentos estoy definiendo una API con mvc 3 y me parecio muy interesante el articulo. ¿Algun recurso sobre como manejar la seguridad en esta nueva version?

  • leonardo

    2/23/2012 7:15:50 AM | Reply

    Hola,
    En principio esto es una beta, yo no saldría con un proyecto en serio, ya tuve experiencias de APIs que cambiaron.
    De movida tenés los mecanismos de ASP y MVC para seguridad, aunque no los probé.
    Gracias por la visita

  • Hector Rosso

    2/24/2012 12:39:58 PM | Reply

    Leonardo,
    Aprecio la claridad de tus posts tanto como la de tus charlas (he asistido a alguna que otra en el mug)
    Estamos buscando la mejor manera de exponer servicios rest. Hemos probado con las adaptaciones que nos da WCF para manejarnos en estos escenarios RESTFull
    Lugo comenzamos a estudiar el tan famoso REST Starter Kit, al cual MS dejo de dar soporte... mas tarde dimos con ek WCF Web API, el tema es que desaparecerá debido a que los equipos de WCF y ASP.NET unificaran todo en lo que denominarían ASP.NET Web API. Esta buenisimo pero no deja de ser una BETA.
    Que me recomendarías usar pensando en una solución aplicable a un ambiente productivo?

    Muchas Gracias

  • leonardo

    2/24/2012 6:14:12 PM | Reply

    Gracias Héctor por el comentario y por asistir a mis charlas.
    Hoy por hoy tenés dos opciones, acciones en un controlador con algún tipo de action filter para filtrar llamadas no AJAX o WCF Web API que en el caso de tener que hacer cosas no muy comunes vas a tener que caer en hacer cosas custom, actualmente estoy haciendo algo de eso pero la verdad es que no veo la hora de que Web API deje de ser beta porque no me cierra del todo, pero es una cuestión de gustos personales.
    Gracias!

Loading