RESTFul en Windows 8

Para las aplicaciones que he realizado para WP7 tenía un helper que me ayudaba a consultar servicios RESTful, pero la verdad, con todo el tema de async, await y Task, es motivo suficiente como para plantearse rescribir cualquier código asíncrono escrito con anterioridad.

Para nuestro helper vamos a necesitar fundamentalmente 2 clases, aunque implementaremos una tercera clase para el tema de serialización.

A todas las clases les vamos a definir una interfaz, las ventajas ya las conocemos: no atamos las clases que usen nuestro helper a nuestra implementación y además, podremos inyectar el helper usando IoC si trabajamos con MVVM.

Empecemos justo por la clase que se encargará de serializar y de-serializar:

public interface IJsonResolver
{
    Task<byte[]> Serialize(object graph);
    Task<T> Deserialize<T>(Stream respnseResult) where T : class;
}

Nuestra interfaz tiene dos métodos que nos permitan serializar y de-serializar nuestros objetos. Estos métodos retornan un Task, por lo que podremos usar su condición asíncrona durante la implementación.

En mi caso, voy a usar Json.net para implementar esta interfaz, pero ustedes pueden elegir su propia implementación.

public class JsonResolver : IJsonResolver
{
    private async Task<string> SerializeToString(object graph)
    {
        return await JsonConvert.SerializeObjectAsync(graph, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
    }

    public async Task<byte[]> Serialize(object graph)
    {
        if (graph is string) return Encoding.UTF8.GetBytes(graph as string);

        var json = await SerializeToString(graph);
        return Encoding.UTF8.GetBytes(json);
    }

    public async Task<T> Deserialize<T>(Stream respnseResult) where T : class
    {
        T result;

        using (var sr = new StreamReader(respnseResult))
        {
            var r = await sr.ReadToEndAsync();
            result = await JsonConvert.DeserializeObjectAsync<T>(r, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
        }

        return result;
    }
}

Ya tenemos nuestra implementación, vamos a lo que de verdad nos interesa.

La utilización de un servicio web, ya sea Rest, Xml o cualquier otro formato, está compuesto principalmente por dos elementos:

  1. El Request: Este elemento podría contener la URL a la cual vamos a realizar la petición, qué tipo de petición vamos a realizar (si es GET, POST, u otra), el ContentType, los parámetros, etc…
  2. Un cliente que realiza la petición usando todas las opciones definidas en el Request.

Siguiendo esta lógica, mi Helper tendrá dos clases más: un RestRequest y un RestClient.

Vamos a definir la interfaz para el RestRequest.

public interface IRestRequest
{
    Uri Url { get; }
    string PostParameter { get; }
    Method Method { get; }
    string Accept { get; }
    string ContentType { get; }

    /// <summary>
    /// Add Parameter to URL. Value is not encode. If Method is POST, parameter is pass in body, if Method is Get, parameter is pass in URL.
    /// </summary>
    /// <param name="name">Parameter name</param>
    /// <param name="value">Parameter value</param>
    void AddParameter(string name, string value);

    /// <summary>
    /// Add Parameter to URL. This method encode parameter value by default
    /// </summary>
    /// <param name="name">Parameter name</param>
    /// <param name="value">Parameter value</param>
    /// <param name="pType"> </param>
    void AddParameter(string name, string value, ParameterType pType);

    /// <summary>
    /// Add Parameter to URL. This method encode parameter value by default
    /// </summary>
    /// <param name="name">Parameter name</param>
    /// <param name="value">Parameter value</param>
    /// <param name="encode"> </param>
    void AddParameter(string name, string value, bool encode);

    /// <summary>
    /// Add Parameter to URL. 
    /// </summary>
    /// <param name="name">Parameter name</param>
    /// <param name="value">Parameter value</param>
    /// <param name="encode">if true, encode parameter value</param>
    /// <param name="pType"></param>
    void AddParameter(string name, string value, bool encode, ParameterType pType);
}

En esta interfaz tenemos varios elementos que nos ayudan a configurar nuestro Request antes de realizar la petición.

El método AddPArameter cuenta con varias sobrecargas ya que vamos a tener varias posibilidades de pasar parámetros. Vamos a verlo uno por uno empezando por el más complejo (el último):

  1. Este método recibe un nombre, un valor, permite indicar si el valor del parámetro debe ser “encodeado” o no y el tipo de parámetro que estamos utilizando.
    1. El “encodear” el valor de un parámetro es usado fundamentalmente cuando pasamos el valor por GET, ya que hay caracteres que no son válidos como parte de la URL
    2. ParamType es un enumerado que me indica el tipo de parámetro que vamos a pasar a nuestro objeto: QueryString (GET?nombre=valor), Post (se pasa en el body) y UrlSegment que es utilizado cuando el parámetro es parte de la URL. Ej. http://odelvalle.com/{parametro}/Rest
  2. Un segundo método en que no es necesario pasar el tipo de parámetro. En este caso el tipo es determinado según el método usado para el Request, si es GET se pasa por QueryString, si es Post, se pasa en el body.
  3. Otro método que nos permite indicar solo el nombre, el valor y el tipo de parámetro. Todos los parámetros pasados mediante este método, el value es “encodeado”.
  4. Finalmente, tenemos un método que nos vale para pasar solo el nombre y el valor. Los parámetros que se pasen usando esta sobrecarga no se van a “encodear” y el tipo de parámetro es determinado por el tipo de Request (GET o POST)

Las propiedades definidas en la interfaz nos brindan el resto de la información necesaria para formar nuestro Request. Vamos a ver la implementación

public enum ParameterType
{
    QueryString,
    Post,
    UrlSegment
}

public enum Method
{
    GET,
    POST,
    PUT,
    DELETE,
    HEAD,
    OPTIONS,
    PATCH
}

public enum ContentType { Json, UrlEncoded }

public class RestRequest : IRestRequest
{
    struct Parameter
    {
        public string Name { get; set; }
        public string Value { get; set; }

        public ParameterType ParameterType { get; set; }
    }

    private readonly IList<Parameter> _parameters;

    private readonly string _url;
        
    public RestRequest(string url, Method method)
    {
        _parameters = new List<Parameter>();
        _url = url;

        Method = method;
    }

    public Uri Url
    {
        get
        {
            var buildUri = new UriBuilder(_url) { Query = BuildQueryString(ParameterType.QueryString) };
            return buildUri.Uri;
        }
    }

    public string PostParameter { get { return BuildQueryString(ParameterType.Post); }}
    public Method Method { get; private set; }

    public string Accept { get { return "application/json"; }}
    public string ContentType { get { return "application/x-www-form-urlencoded"; } }

    public void AddParameter(string name, string value)
    {
        AddParameter(name, value, false);
    }

    public void AddParameter(string name, string value, ParameterType pType)
    {
        AddParameter(name, value, true, pType);
    }

    public void AddParameter(string name, string value, bool encode)
    {
        AddParameter(name, value, encode, (Method == Method.GET) ? ParameterType.QueryString : ParameterType.Post );
    }

    public void AddParameter(string name, string value, bool encode, ParameterType pType)
    {
        var val = encode ? Uri.EscapeDataString(value) : value;

        if (pType == ParameterType.UrlSegment) _url.Replace(string.Format("{{0}}", name), val);
        else _parameters.Add(new Parameter { Name = name, Value = val, ParameterType = pType});
    }

    private string BuildQueryString(ParameterType pType)
    {
        var plist = _parameters.Where(p=> p.ParameterType == pType).Select(p => string.Concat(p.Name, "=", p.Value));
        return String.Join("&", plist.ToArray());
    }
}

Al constructor de la clase RestRequest le vamos a pasar la URL a la cual queremos realizar la petición y el método a utilizar.

Ya con esto tenemos nuestro RestRequest, vamos a ver el cliente que realizará la petición. Empezamos viendo la interfaz:

public interface IRestClient
{
    Task<T> Execute<T>(IRestRequest request) where T : class;
}

.. y eso es todo. No necesitamos nada más ya que estamos definiendo un cliente que recibirá un objeto RestRequest y hará una petición. Veamos la implementación.

public class RestClient : IRestClient
{
    private readonly IJsonResolver _resolver;

    public RestClient(IJsonResolver resolver)
    {
        _resolver = resolver;
    }

    public async Task<T> Execute<T>(IRestRequest request) where T : class 
    {
        var wr = WebRequest.CreateHttp(request.Url);

        wr.Method = request.Method.ToString();
        wr.Accept = request.Accept;
        wr.ContentType = request.ContentType;

        if (!string.IsNullOrEmpty(request.PostParameter))
        {
            return await ExecuteWithData<T>(wr, request);
        }

        var response = await wr.GetResponseAsync();
        return await _resolver.Deserialize<T>(response.GetResponseStream());
    }

    private async Task<T> ExecuteWithData<T>(WebRequest wr, IRestRequest request) where T : class
    {
        var body = await _resolver.Serialize(request.PostParameter);

        using (var stream = await wr.GetRequestStreamAsync())
        {
            await stream.WriteAsync(body, 0, body.Length);
        }

        var response = await wr.GetResponseAsync();
        return await _resolver.Deserialize<T>(response.GetResponseStream());
    }
}

Nuestro cliente recibe en el constructor el RestRequest que se desea realizar. Mediante el método Execute, el cliente se encarga de realizar la petición y si hay respuesta del servicio Rest, entonces nos retornará el objeto listo para usarse.

Para probar todo esto y ver lo simple que es de usar, me busqué alguna ejemplo Rest que ya existiera para Windows 8.  Aquí tenéis una: Metro client for Web API CRUD

En esta aplicación al final de la clase MainViewModel.cs tenemos el GET usando HttpClient y DataContractJsonSerializer.

using (var http = new HttpClient())
{
    var resp = await http.GetAsync(new Uri(ApiRoot));
    using (var stream = await resp.Content.ReadAsStreamAsync())
    {
        var djs = new DataContractJsonSerializer(typeof(List<Person>));
        People = new ObservableCollection<Person>((IEnumerable<Person>)djs.ReadObject(stream));
    }
}

Vamos a quitar ese código y vamos a usar el nuestro:

var request = new RestRequest(ApiRoot, Method.GET);
var restFul = new RestClient(new JsonResolver());

People = new ObservableCollection<Person>(await restFul.Execute<IEnumerable<Person>>(request));

… ¿probamos?

screenshot_10112012_004023

Aquí dejo la aplicación de ejemplo modificada y usando nuestro Helper.

PD: Aún no he probado todo el Helper, así que es posible que en un escenario específico pueda aparecer algún BUM Sonrisa

Un comentario en “RESTFul en Windows 8”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *