REST 101 – Parte 3 – ASP.net MVC para desenvolvimento de serviços REST

Posted on fevereiro 23, 2011 by

0


Olá pessoal, tudo certo?

Depois de muito tempo, volto a tratar de REST aqui no blog.

Perceba que:

  • No primeiro post dessa série, mostrei como implementar um serviço REST, muito simples, usando WCF.
  • No segundo, apresentei alguns conceitos fundamentais relacionados a REST.
  • Hoje, vou apresentar alguns truques que habilitam o ASP.net MVC  como alternativa para construção de serviços REST.

Nesse post, presumo que você já saiba, mesmo que superficialmente, como utilizar ASP.net MVC para construir aplicações WEB. Este não é um post introdutório.

Sem mais delongas,

Algumas (poucas) palavras sobre ASP.net MVC

ASP.net MVC é o novo framework da Microsoft para desenvolvimento de aplicativos Web. De forma bastante ágil, ele entrega todo o poder do ASP.net com a potencialidade do Pattern Arquitetônico Model-View-Controller.

Ao meu ver, ASP.net MVC representa uma mudança radical na forma como desenvolvedores utilizam ASP.net. Ele enfatiza uma arquitetura mais “limpa”, bons padrões de design, testabilidade, entre outras “boas” coisas.

Com ASP.net MVC, URIs deixam de endereçar arquivos no servidor para endereçar “Actions” e “Controllers”.

Routing?! Não obrigado (só um pouco)

Uma das caracteristicas mais bacanas do ASP.net MVC é a simplicidade com que conseguimos fazer determinados “padrões” de endereçamento apontar para Controllers e Actions. Concorda?!

O único problema (se é que podemos chamar de problema) dessa funcionalidade está em seu caráter (geralmente) estático. Normalmente, as rotas (padrões de endereçamento, controllers, actions e parâmetros) são definidas durante a codificação.

Considere o desenvolvimento de um serviço REST onde o padrão de endereçamento não seja tão bem comportado. Livre-se de seus preconceitos e considere o seguinte cenário hipotético:

http:///customers/CA373/orders/ABCKG/items/1

Não criaria um endereçamento de recurso como esse, mas, entretanto, ele é válido! O endereço que forneci no exemplo se refere ao primeiro item, do pedido código ABCKG, realizado pelo cliente código CA373. Como você trataria esse padrão de endereçamento no routing de seu serviço ASP.net MVC? Eis a minha proposta:


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

    routes.MapRoute(
        "Default", // Route name
        "{*url}", // URL with parameters
        new { controller = "Default", action = "Index", id = UrlParameter.Optional } // Parameter defaults
    );
}

            

Simples e bonitinho! O “pattern” fornecido para o método MapRoute é compatível com os utilizados para UriTemplate. Assim, o coringa ( * ) representa “o resto do endereço”.

Eis a minha implementação para esse controller:


              
using System.Web.Mvc;

namespace CoolMvc.Controllers
{
    public class DefaultController : Controller
    {
        public string Index(string url)
        {
            return "URL: " + url;
        }
    }
}

            

Na prática,  transferimos para nosso método Index a responsabilidade de processar o “request”. Qualquer que seja o endereço fornecido para nosso serviço, este será “processado” por essa action. Observe:

image

Mesmos parâmetros, mais de um verbo, uma mesma rota

O mecanismo de Routing do ASP.net MVC é fenomenal. Ele reconhece o verbo HTTP de um request e, de acordo com o pattern de endereço, encaminha para a Action e Controller adequados. Para que essa mágica ocorra, basta:

  1. que o nome do método seja igual ao nome da Action;
  2. que o verbo correspondente a action esteja sinalizado como meta-atributo.

O “sem-graça” dessa história é o C#. Afinal, ele não diferencia dois métodos pelos seus meta-atributos. Esse “desvio de conduta” do C# faz com que tenhamos de adicionar parâmetros dummies em nossas actions (geralmente associadas a verbos POST).

Felizmente, há uma alternativa! Considere o serviço REST iniciado na seção anterior e perceba como evito a necessidade de utilizar atributos dummies:


              
using System.Web.Mvc;

namespace CoolMvc.Controllers
{
    public class DefaultController : Controller
    {
        [ActionName("Index")]
        [AcceptVerbs("GET")]
        public string HandleGet(string url)
        {
            return "GET URL: " + url;
        }

        [ActionName("Index")]
        [AcceptVerbs("POST")]
        public string HandlePost(string url)
        {
            return "POST URL: " + url;
        }
    }
}

            

Meu segredo foi identificar a Action usando o atributo ActionName.

Para testar, podemos usar o Fiddler, ou o curl. Segue o resultado dos meus testes usando o curl.


              
C:\curl>curl http://localhost:49278/customers/CA373/orders/ABCKG/items/1
GET URL: customers/CA373/orders/ABCKG/items/1
C:\curl>curl -d "" http://localhost:49278/customers/CA373/orders/ABCKG/items/1
POST URL: customers/CA373/orders/ABCKG/items/1
C:\curl>

            

Acertando o Status de Retorno HTTP

REST está fundamentado na utilização adequada dos verbos e status de retorno do HTTP. Se um recurso não existe, retorna-se 404. Se houve uma falha interna, retorna-se 500. Se o usuário não possui acesso a um recurso, retorna-se 403.

ASP.net MVC 3 possui um ActionResult preparada para retornar códigos de status HTTP adequados. Observe:


              
using System.Web.Mvc;

namespace CoolMvc.Controllers
{
    public class DefaultController : Controller
    {
        [ActionName("Index")]
        [AcceptVerbs("GET")]
        public ActionResult HandleGet(string url)
        {
            return new HttpStatusCodeResult(500, "Server error!");
        }
    }
}

            

            

Verbo cretino (sempre rertorna erro 500), mas útil (demonstra o funcionamento do novo HttpStatusCodeResult). Observe o resultado no fiddler:

image

Funciona!

Se você ainda está utilizando o ASP.net MVC 2, não se desespere… Segue um Result (menos genérico) que escrevi para retornar 404.


              
using System.Web.Mvc;
using System.IO;
using System.Text;

namespace CoolMvc.ActionResults
{
    public class Http404Result : HttpNotFoundResult
    {
        public override void ExecuteResult(ControllerContext context)
        {
            context.HttpContext.Response.StatusCode = 404;
            context.HttpContext.Response.ContentType = "text/html";

            using (StreamWriter wtr = new StreamWriter(context.HttpContext.Response.OutputStream))
            {
                StringBuilder sb = new StringBuilder();
                sb.Append("CoolMvc");
                sb.Append("The resource you requested cannot be located.
"); sb.Append(""); wtr.WriteLine(sb.ToString()); wtr.Flush(); wtr.Close(); } Stream stream = context.HttpContext.Response.OutputStream; if (stream != null && stream.CanSeek) { context.HttpContext.Response.Headers["Content-Length"] = stream.Length.ToString(); } } } }

            

Simples e bonito! Como você pode observar, não é difícil escrever uma versão “particular” de HttpStatusCodeResult.

Retornando um recurso conforme especificado no Request

Outra característica comum de serviços REST é permitr que o “cliente” especifique, no cabeçalho, em que formato deseja receber o recurso solicitado. Eis minha solução para identificar qual formato foi especificado em ASP.net MVC:


              
using System.Web.Mvc;

namespace CoolMvc.Controllers
{
    public class DefaultController : Controller
    {
        [ActionName("Index")]
        [AcceptVerbs("GET")]
        public ActionResult HandleGet(string url)
        {
            if (HttpContext.Request.AcceptTypes.Length > 0)
            {
                foreach (string type in HttpContext.Request.AcceptTypes)
                {
                    switch (type.ToLower())
                    {
                        case "text/html":
                        case "*/*":
                            return new HtmlResult(url);
                        case "text/xml":
                            return new XmlResult(url);
                        case "application/json":
                            return new JSonResult(url);
                    }
                }
            }
            return new NotAcceptableGetResult();
        }
    }
}

            

Simples, não?! Observe que, para esse exemplo, criei um ActionResult para renderizar cada formato suportado.

Apenas para fins ilustrativos, veja o result responsável por retornar os formatos aceitos:


              
using System;
using System.Text;
using System.Web.Mvc;
using System.Net;
using System.IO;

namespace CoolMvc.ActionResults
{
    class NotAcceptableGetFormatResult : ActionResult
    {
        public override void ExecuteResult(ControllerContext context)
        {
            context.HttpContext.Response.StatusCode = (Int32)HttpStatusCode.NotAcceptable;
            context.HttpContext.Response.Headers.Add
                                                ("Allow", "text/html,text/xml,application/json");
            context.HttpContext.Response.ContentType = "text/html";
            using (StreamWriter wtr = new StreamWriter(
                                        context.HttpContext.Response.OutputStream)
            {
                StringBuilder sb = new StringBuilder();
                sb.Append("CoolMvc");
                sb.Append(
                "The accepted types you requested are not supported by this service.
"); sb.Append( "Please request */* (returns HTML), text/html, text/xml, or application/json."); sb.Append(""); wtr.WriteLine(sb.ToString()); wtr.Flush(); wtr.Close(); } } } }

            

Acho que você pegou a idéia!

Por hoje, era isso!

Smiley piscando

Posted in: AspNetMvc, C#, REST