[Web API] AtomPub y Web API Soporte para Multimedia

Esta semana agregué a este sitio soporte para el Atom Publishing Protocol, ayudado por supuesto por esta excelente guía de Ben Foster que en una serie de 3 artículos explica como integrar el AtomPub a un API en ASP.NET Web API, al final de los tres artículos queda haciendo falta cubrir un contenido, que de hecho lo piden en uno de los comentarios y que yo también necesito para publicar las imágenes de mi sitio desde el Windows Live Writer, pero vamos, somos desarrolladores y no conocemos los imposibles :P, así en este articulo voy a describir como lo solucioné.

¿Que dice la documentación?

Si revisamos la documentación oficial del protocolo encontramos que tenemos en el Service Document hay un elemento llamado app:accept y ya nos da una idea que vamos a necesitar, por ejemplo agregar “image/png”. Si continuamos leyendo esta documentación encontramos la sección de Media Resources and Media Link Entries donde nos brinda la documentación de como deben ir las peticiones y como se debe responder desde el servicio. Por si fuera poco y como bonus me encontré con el blog de Joe Cheng, quien explica en este articulo explica como deben estar las colecciones para que el WLW sea capaz de encontrar una colección donde ubicar dichos elementos (las imágenes). Teniendo ya bien clara toda la documentación solo nos hace falta implementarlo 🙂

La implementación

Lo primero que hice es permitir describir una colección para mis imágenes en el Service Document, esto es en el GET de uno de los controladores de mi API, el código queda de la forma:

    public class ServicesController : ApiController
    {
        public HttpResponseMessage Get()
        {
            var serviceDocument = new ServiceDocument();
            var workSpace = new Workspace
                                {
                                    Title = new TextSyndicationContent("Nicoloco Site"),
                                    BaseUri = new Uri(Request.RequestUri.GetLeftPart(UriPartial.Authority))
                                };
            var posts = new ResourceCollectionInfo("Nicoloco Blog",
                                                   new Uri(Url.Link("DefaultApi", new { controller = "blog" })));

            posts.Accepts.Add("application/atom+xml;type=entry");

            var images = new ResourceCollectionInfo("Images Blog",
                                                    new Uri(Url.Link("DefaultApi", new { controller = "images" })));
            images.Accepts.Add("image/png");
            images.Accepts.Add("image/jpeg");
            images.Accepts.Add("image/gif");

            workSpace.Collections.Add(posts);
            workSpace.Collections.Add(images);

            serviceDocument.Workspaces.Add(workSpace);

            var response = new HttpResponseMessage(HttpStatusCode.OK);
            var formatter = new AtomPub10ServiceDocumentFormatter(serviceDocument);
            var stream = new MemoryStream();
            using (var writer = XmlWriter.Create(stream))
            {
                formatter.WriteTo(writer);
            }
            stream.Position = 0;
            var content = new StreamContent(stream);
            response.Content = content;
            response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/atomsvc+xml");
            return response;
        }
    }

Lo que hice fue basarme en el código del articulo de Ben y agregar una nueva colección como lo indica la documentación que veíamos antes, ahora se puede descubrir esta colección para ubicar las imágenes

Una vez se puede descubrir esta colección en el documento del servicio debemos definir una entidad que nos ayude con la información de estos recursos y que podamos pedir como parámetro en nuestros controladores, mi entidad quedo definida de la forma:

    public class PostImage
    {
        public string Title { get; set; }
        public string FileName { get; set; }
        public string ContentType { get; set; }
        public byte[] BytesImage { get; set; }
        public DateTime PublishDate { get; set; }
        public string Url { get; set; }
    }

Esta definición va ligada a la documentación del protocolo, donde indican que cuando se pida dicho recurso es necesario conocer un titulo, ContentType tipo del recurso etc.

Como los request que vamos a recibir vendrán con una cabecera ContentType: image/* es necesario agregar un MediaTypeFormatter que nos facilita el tema de la negociación de contenido en Web API, el código queda de la forma:

public class ImageFormatter : MediaTypeFormatter
    {
        private const string Png = "image/png";
        private const string Jpeg = "image/jpeg";
        private const string Gif = "image/gif";
        SupportedMediaTypes.Add(new MediaTypeHeaderValue(Atom));

        public ImageFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue(Png));
            SupportedMediaTypes.Add(new MediaTypeHeaderValue(Jpeg));
            SupportedMediaTypes.Add(new MediaTypeHeaderValue(Gif));
            SupportedMediaTypes.Add(new MediaTypeHeaderValue(Atom));
        }
        public override bool CanReadType(Type type)
        {
            return typeof(PostImage) == type;
        }
        public override bool CanWriteType(Type type)
        {
            return typeof(PostImage) == type;
        }
        public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, System.Net.Http.HttpContent content, IFormatterLogger formatterLogger)
        {
            return Task.Factory.StartNew(() =>
                                             {
                                                 if (type == typeof(PostImage))
                                                 {
                                                     var postImage = (PostImage)Activator.CreateInstance(type);

                                                     //string name = content.Headers.GetValues("slug").FirstOrDefault();
                                                     MediaTypeHeaderValue contentType = content.Headers.ContentType;
                                                     string fileExtension = contentType.MediaType.Replace("image/", string.Empty);
                                                     postImage.ContentType = contentType.MediaType;
                                                     postImage.Title = DateTime.Now.ToString("yyyyMMddHHmmssFF");
                                                     postImage.FileName = string.Format("{0}.{1}", postImage.Title,
                                                                                        fileExtension);
                                                     var memoryStream = new MemoryStream();
                                                     readStream.CopyTo(memoryStream);
                                                     postImage.BytesImage = memoryStream.ToArray();
                                                     return (object)postImage;
                                                 }
                                                 return null;
                                             });

        }

        public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, System.Net.Http.HttpContent content, System.Net.TransportContext transportContext)
        {
            return Task.Factory.StartNew(() =>
                                             {
                                                 var image = (PostImage)value;
                                                 var item = new SyndicationItem
                                                                {
                                                                    Title = new TextSyndicationContent(image.Title),
                                                                    BaseUri = new Uri(image.Url),
                                                                    LastUpdatedTime = image.PublishDate,
                                                                    Content = new UrlSyndicationContent(new Uri(image.Url), image.ContentType)
                                                                };
                                                 item.Authors.Add(new SyndicationPerson { Name = "Nicolocodev" });
                                                 using (XmlWriter writer = XmlWriter.Create(writeStream))
                                                 {
                                                     var atomformatter = new Atom10ItemFormatter(item);
                                                     atomformatter.WriteTo(writer);
                                                 }
                                             });
        }
    }

Con lo anterior logramos entender la petición y transformarla a algo que entendamos y además somos capaces de brindar el recurso como el cliente lo desea cuando nos indique :).

Con esto solo nos falta agregar el tema de la persistencia del lado del controlador que se encargará de controlar estas peticiones, en mi caso, como dije en el primer articulo de este blog, haré uso de WindowsAzure Storage, así que para el almacenamiento de imágenes usaré el Blob Storage, la implementación de mi código queda de la forma:

    public class ImagesController : ApiController
    {
        private readonly CloudBlobContainer _container;
        private const string ImagesDirectory = "images";
        public ImagesController()
        {
            CloudBlobClient blobClient = WindowsAzureConfig.StorageAccount.CreateCloudBlobClient();
            _container = blobClient.GetContainerReference("blog");
            _container.CreateIfNotExists();
            _container.SetPermissions(new BlobContainerPermissions
                                         {
                                             PublicAccess = BlobContainerPublicAccessType.Blob
                                         });
            _container.GetDirectoryReference(ImagesDirectory);
        }

        public HttpResponseMessage GetImage(string id)
        {
            CloudBlockBlob blockBlob = _container.GetDirectoryReference(ImagesDirectory).GetBlockBlobReference(id);
            blockBlob.FetchAttributes();
            var postImage = new PostImage
                                {
                                    Title = id,
                                    ContentType = blockBlob.Properties.ContentType,
                                    FileName = id,
                                    Url = blockBlob.Uri.AbsoluteUri,
                                    PublishDate = DateTime.Now
                                };
            return Request.CreateResponse(HttpStatusCode.OK, postImage);
        }

        public HttpResponseMessage PostImageImage(PostImage postImage)
        {
            CloudBlockBlob blockBlob = _container.GetDirectoryReference(ImagesDirectory).GetBlockBlobReference(postImage.Title);
            blockBlob.Properties.ContentType = postImage.ContentType;

            var stream = new MemoryStream(postImage.BytesImage);
            blockBlob.UploadFromStream(stream);

            postImage.Url = blockBlob.Uri.AbsoluteUri;
            var response = Request.CreateResponse(HttpStatusCode.Created, postImage);
            response.Headers.Location = new Uri(Url.Link("DefaultApi", new { controller = "images", id = postImage.Title }));
            return response;
        }
    }

Espero les sea de utilidad.

Hasta el próximo post.

Anuncios
[Web API] AtomPub y Web API Soporte para Multimedia

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s