Hola a todos, en este post quiero mostrar una forma en la cual es posible implementar dos temas importantes al crear un API Rest (no por usar Web API nuestra API el REST o RESTFull), el primero es la identificación de recursos y el otro el concepto de HATEOAS (Hypermedia as the engine of application state) (abajo aclaro más este tema), lo anterior con el fin de estar un poco más alineado al modelo de madurez de rest.


Aclaro que es una aproximación, y dependiendo del escenario, puede ser necesario ampliar la implementación.

Primero veamos que tenemos, partimos de dos sencillas clases (Post y Comments):


public class Post
{
    public int Id { get; set; }

    public string Title { get; set; }

    public string Description { get; set; }

    public string Url { get; set; }

    public List<string> Tags { get; set; }

    public DateTime PublishedDate { get; set; }

    public virtual List<Comment> Comments { get; set; }
}

public class Comment
{
    public int Id { get; set; }

    public string Description { get; set; }

    public virtual Post Post { get; set; } 
}

Ahora, una clase BlogService que se va a encargar de retornar el listado de post, para no complicar el ejemplo, los datos provienen de una lista, pero en otro escenario tan solo es consutarlos de la base de datos:


public class BlogService
{
    public IEnumerable<Post> GetAllPosts()
    {
        return posts;
    }

    public Post GetPost(int id)
    {
        return posts.FirstOrDefault(c => c.Id == id);
    }

    private readonly List<Post> posts = new List<Post>()
    {
        new Post()
        {
            Id = 1,
            Title = "[ASPNET Web API] Controllar la cantidad de request permitidos",
            Description = "Hola a todos, en este post quiero mostrar cómo es posible controlar la cantidad de request permitidos a un endpoint de Web API, para prevenir alteración de datos y/o degradamiento en el performance del servicio...",
            PublishedDate = DateTime.Now.AddDays(-1),
            Url = @"http://www.julitogtu.com/2015/08/10/aspnet-web-api-controllar-la-cantidad-de-request-permitidos/",
            Tags = new List<string>() { "aspnet","web api"},
            Comments = new List<Comment>()
            {
                new Comment() { Id = 1, Description = "Great Post"},
                new Comment() { Id = 2, Description = "Great Post"},
                new Comment() { Id = 3, Description = "Great Post"}
            }
        },
        new Post()
        {
            Id = 2,
            Title = "[ASPNET Web API] Acciones centralizadas",
            Description = "Hola a todos, una práctica común al trabajar con Web API es tener una clase base, y allí un método que sea el encargado de ejecutar el código definido dentro de cada una de las acciones, ya que esto facilita el control y permite centralizar acciones comunes cómo el manejo...",
            PublishedDate = DateTime.Now.AddDays(-1),
            Url = @"http://www.julitogtu.com/2015/07/09/webapi-acciones-centralizadas/",
            Tags = new List<string>() { "aspnet","web api"},
            Comments = new List<Comment>()
            {
                new Comment() { Id = 4, Description = "Great Post"},
                new Comment() { Id = 5, Description = "Great Post"},
                new Comment() { Id = 6, Description = "Great Post"}
            }
        },
        new Post()
        {
            Id = 3,
            Title = "[Kendo UI] Enlazar un GridView a un Endpoint de Web API OData",
            Description = "Hola a todos, en un post anterior, vimos cómo implementar paginación en Web API, y en está ocasión también vamos a paginar los resultados pero en este caso implementando OData con Web API y haciendo binding a un GridView de Kendo UI...",
            PublishedDate = DateTime.Now.AddDays(-1),
            Url = @"http://www.julitogtu.com/2015/07/06/kendo-ui-enlazar-un-gridview-a-un-endpoint-de-web-api-odata/",
            Tags = new List<string>() { "aspnet","web api", "odata", "kendo UI"},
            Comments = new List<Comment>()
            {
                new Comment() { Id = 7, Description = "Great Post"},
                new Comment() { Id = 8, Description = "Great Post"}
            }
        },
        new Post()
        {
            Id = 4,
            Title = "[ASPNET MVC] Gráficas usando Kendo UI Chart Scaffolding",
            Description = "Hola a todos, dentro de las novedades del Kendo UI Q2 2015, aparece una novedad interesante, y es la opción de usar Scaffolding para gráficas, así el tiempo necesario de implementación del Helper para Charts es reducido considerablemente y es tan sencillo con seguir unos cuantos pasos...",
            PublishedDate = DateTime.Now.AddDays(-1),
            Url = @"http://www.julitogtu.com/2015/07/01/aspnet-mvc-kendo-ui-chart-scaffolding/",
            Tags = new List<string>() { "aspnet","mvc", "kendo UI"},
            Comments = new List<Comment>()
            {
                new Comment() { Id = 9, Description = "Great Post"},
                new Comment() { Id = 10, Description = "Great Post"}
            }

        },
        new Post()
        {
            Id = 5,
            Title = "[ASPNET Web API] Serializando JSON en notación CamelCase",
            Description = "Hola a todos, hoy simplemente un sencillo tip para personalizar el JSON de respuesta en notación CamelCase, la cual utiliza JavaScript...",
            PublishedDate = DateTime.Now.AddDays(-1),
            Url = @"http://www.julitogtu.com/2015/07/01/aspnet-mvc-kendo-ui-chart-scaffolding/",
            Tags = new List<string>() { "aspnet","web api"},
            Comments = new List<Comment>()
            {
                new Comment() { Id = 11, Description = "Great Post"},
                new Comment() { Id = 12, Description = "Great Post"}
            }
        }
    };
}

Ahora, creamos un controlador que retorne esos datos:


public class PostsController : ApiController
{
    private readonly BlogService _blogService = new BlogService();

    public IEnumerable<Post> GetAll()
    {
        return _blogService.GetAllPosts();
    }

    public Post Get(int id)
    {
        return _blogService.GetPost(id);
    }
}

Hasta el momento nada raro, pero si quieres puedes encapsular la ejecución de las acciones cómo lo muestro en este post, ahora al probar tenemos:

alt

Ahora, si analizamos la respuesta, se puede ver que:

  • No implementa el concepto de identificación de recursos, ya que retorna el Id de cada post y comentario más no una URL que permita obtener dicho recurso, en ese caso es necesario construirla.
  • No implementa el concepto de HATEOAS, el cual define que la respuesta debe tener una "sección" de links que permita navegar sobre los resultados.

Así que iniciemos, primero voy a crear una clase que tiene cómo función encapsular el formato de la respuesta, en donde vamos a tener dos propiedades Links (para HATEOAS) y Entries para los datos.


public class PostModel
{
    public IEnumerable<LinkModel> Links { get; set; }

    public IEnumerable<PostEntryModel> Entries { get; set; }
}

Veamos ahora la clase LinkModel, que tiene sólo dos propiedades, la URL y en la propiedad Rel va a estar el tipo de link, cómo por ejemplo self para el actual, nextpage y prevpage para ir navegación si tenemos resultados paginados, etc:


public class LinkModel
{
    public string Url { get; set; }

    public string Rel { get; set; }
}

Y la otra clase implicada es PostEntryModel, que tiene el título y la URL del post (acá retornamos solo lo que realmente necesitamos), la URL del recurso y el listado de comentarios:


public class PostEntryModel
{
    public string Title { get; set; }

    public string PostUrl { get; set; }

    public string Url { get; set; }

    public IEnumerable<CommentEntryModel> Comments { get; set; }
}

Y sólo falta la clase CommentEntryModel que solo tiene el texto del comentario y la URL del recurso:


public class CommentEntryModel
{
    public string Url { get; set; }

    public string Comment { get; set; }
}

Hasta este momento solo hemos creado unas clases que nos van a ayudar a construir la respuesta más enfocada a lo que dice REST, y realmente, mucho más clara y explicativa.

Bien, la siguiente clase a crear (y última), es ModelFactory, ModelFactory tiene el objetivo de crear la clase PostModel y añadir los links necesarios, así que primero miremos la clase:


public class ModelFactory
{
    private readonly UrlHelper _urlHelper;

    public ModelFactory(HttpRequestMessage request)
    {
        _urlHelper = new UrlHelper(request);
    }

    public PostModel Create(List<Post> postList)
    {
        var postModel = new PostModel()
        {
            Links = new List<LinkModel>()
            {
               new LinkModel()
               {
                   Rel = "self",
                   Url = _urlHelper.Link("Post",null),
               }  
            },
            Entries = CreatePost(postList)
        };

        return postModel;
    }

    private IEnumerable<PostEntryModel> CreatePost(IEnumerable<Post> postList)
    {
        return postList.Select(post => new PostEntryModel()
        {
            Title = post.Title,
            PostUrl = post.Url,
            Url = _urlHelper.Link("Post", new { id = post.Id }),
            Comments = CreateComments(post.Id, post.Comments)
        }).ToList();
    }

    private IEnumerable<CommentEntryModel> CreateComments(int idPost, IEnumerable<Comment> comments)
    {
        return comments.Select(comment => new CommentEntryModel()
        {
            Comment = comment.Description,
            Url = _urlHelper.Link("Comment", new { postid = idPost, id = comment.Id }),
        }).ToList();
    }
}

En ModelFactory, se crea una instancia de UrlHelper, que permite obtener el link para una determinada ruta mediante el método Link, y así solucionamos el problema de la identificación de recursos, luego el método público es Create que retorna PostModel y recibe el listado de post cómo parámetro, es decir, los datos tal cómo vienen de la consulta a la lista (o base de datos en un ambiente real).


En el método Create, se crea una instancia de PostModel, para la colección solo voy a crear uno, y es el self, cómo se puede observar se usa _urlHelper.Link pasándole primero el nombre de la ruta (del archivo WebApiConfig) y luego los datos que requiere, en este caso null.

El código para Entries es bastante sencillo, basicamente se itera sobre el listado de post para formatear tanto los comentarios cómo la información del post cómo tal, igualmente se utiliza _ulrHelper.Link para crear el link al recurso con el nombre de cada ruta particular.

A la clase BlogService le hacemos un par de cambios:

  • Definirle un constructor que reciba por parámetro el request, para instanciar la clase UrlHelper
  • Añadir un nuevo método GetPostModel que retorna PostModel

Entonces luego de ese par de cambios, la clase BlogService queda:


public class BlogService
{
    private ModelFactory modelFactory;

    public BlogService() { }

    public BlogService(HttpRequestMessage request)
    {
        modelFactory = new ModelFactory(request);
    }

    public IEnumerable<Post> GetAllPosts()
    {
        return posts;
    }

    public Post GetPost(int id)
    {
        return posts.FirstOrDefault(c => c.Id == id);
    }

    public PostModel GetPostModel()
    {
        return modelFactory.Create(posts);
    }

    private readonly List<Post> posts = new List<Post>()
    {....};
}

En la clase WebApiConfig, las rutas están:


public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services
        var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().FirstOrDefault();
        if (jsonFormatter != null)
            jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "Comment",
            routeTemplate: "api/post/{postid}/comment/{id}",
            defaults: new { controller = "comment"}
        );

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

Para validar el cambio, creamos un nuevo controlador, Posts2Controller:


public class Posts2Controller : ApiController
{
    private BlogService _blogService;

    public PostModel GetAll(HttpRequestMessage request)
    {
        _blogService = new BlogService(request);
        return _blogService.GetPostModel();
    }

    public Post Get(HttpRequestMessage request, int id)
    {
        _blogService = new BlogService(request);
        return _blogService.GetPost(id);
    }
}

Y si ahora probamos, la respuesta es mucho más clara y descriptiva:

alt

Y hemos llegado al final, espero el post les sea interesante, y de tarea les queda ajustar el formato cuando para el detalle de un post y del comentario, saludos!