[REST WCF] - Conditional GET - Cuidado con la comprobación de If-Modified-Since

Leyendo la entrada de Unai http://geeks.ms/blogs/unai/archive/2009/04/21/wcf-rest-conditional-get-save-bandwidth.aspx (Muy recomendable para la gente que quiera aprender REST) tengo que comentar algo acerca de la comprobación de If-Modified-Since en el condicional GET.

Aclaro desde ya, que todo lo que voy a comentar en esta entrada está en el libro de la persona que para mí más sabe de RESTful .NET del mundo, Jon Flanders, sí quieres aprender de verdad no dejes de visitar su blog y leer su libro RESTful .NET

Al tajo:

El problema esta en el valor date/time que se envía en la cabecera Last-Modified y la perdida de precisión que se produce, de tan sólo un segundo. Como dice Unai, vamos a verlo en código, que todo se explica mucho mejor:

[DataContract(Name = "product", Namespace = "")]
public class Product
{
    [DataMember(Name = "id", Order = 1)]
    public string Id;
    [DataMember(Name = "description", Order = 2)]
    public string Description;
    [DataMember(Name = "price", Order = 3)]
    public decimal Price;
    [DataMember(Name = "lastModified", Order = 4)]
    public DateTime LastModified;
}
/// <summary>
/// Gets the product by id.
/// </summary>
/// <param name="id">The id.</param>
/// <returns></returns>
public Product GetProductById(string id)
{
    var product = _products.SingleOrDefault(p => p.Id == id);
 
    if (CheckLastModified(product))
    {
        return null;
    }
 
    if (product == null)
    {
        OutgoingWebResponseContext ctx = WebOperationContext.Current.OutgoingResponse;
        ctx.SetStatusAsNotFound();
        ctx.SuppressEntityBody = true;
    }
 
    SetLastModified(product);
 
    return product;
}

 

/// <summary>
/// Checks the last modified.
/// </summary>
/// <param name="p">The p.</param>
/// <returns>bool</returns>
private bool CheckLastModified(Product p)
{
    IncomingWebRequestContext ctx = WebOperationContext.Current.IncomingRequest;
 
    string lastModified =
        ctx.Headers[HttpRequestHeader.IfModifiedSince];
 
    if (lastModified != null)
    {
        DateTime dt = DateTime.Parse(lastModified);
        if (p.LastModified == dt)
        {
            SetNotModified();
            return true;
        }
    }
    return false;
}

 

/// <summary>
/// Sets the last modified.
/// </summary>
/// <param name="p">The p.</param>
private void SetLastModified(Product p)
{
    OutgoingWebResponseContext ctx =
        WebOperationContext.Current.OutgoingResponse;
    ctx.LastModified = p.LastModified;
}

 

Consultamos un producto:

rest1

Nos devuelve un 200 y la cabecera Last-Modified viene informada.

rest2

Recuperamos otra vez el mismo producto, pero ahora le informamos el If-Modified-Since y…

rest3

Ehhh??? Un 200??? Si estabamos esperando un 304???

Pues ahí está el problema del que habla Jon, la perdida de precisión en Last-Modified. Así que vamos a modificar el código de nuestro servicio para que nos devuelva un 304:

Lo primero cambiamos el tipo de dato de la propiedad LastModified a DateTimeOffset

[DataContract(Name = "product", Namespace = "")]
public class Product
{
    [DataMember(Name = "id", Order = 1)]
    public string Id;
    [DataMember(Name = "description", Order = 2)]
    public string Description;
    [DataMember(Name = "price", Order = 3)]
    public decimal Price;
    [DataMember(Name = "lastModified", Order = 4)]
    public DateTimeOffset LastModified;
}

 

Modificamos la función CheckLastModified:

/// <summary>
/// Checks the last modified.
/// </summary>
/// <param name="p">The p.</param>
/// <returns>bool</returns>
private bool CheckLastModified(Product p)
{
    IncomingWebRequestContext ctx = WebOperationContext.Current.IncomingRequest;
 
    string lastModified =
        ctx.Headers[HttpRequestHeader.IfModifiedSince];
 
    if (lastModified != null)
    {
        DateTimeOffset dt = DateTimeOffset.Parse(lastModified);
        if (InternalDateTimeCompare(p.LastModified.UtcDateTime, dt))
        {
            SetNotModified();
            return true;
        }
    }
    return false;
}

Modificamos también SetLastModified:

/// <summary>
/// Sets the last modified.
/// </summary>
/// <param name="p">The p.</param>
private void SetLastModified(Product p)
{
    OutgoingWebResponseContext ctx =
        WebOperationContext.Current.OutgoingResponse;
    ctx.LastModified = p.LastModified.DateTime;
}

Y por último añadimos la siguiente función que comparará las fechas al nivel de precisión necesario para poder enviar ese 304 que tanto deseamos:

/// <summary>
/// Internals the date time compare.
/// </summary>
/// <param name="dateTime">The date time.</param>
/// <param name="dt">The dt.</param>
/// <returns>bool</returns>
private bool InternalDateTimeCompare(DateTime dateTime, DateTimeOffset dt)
{
    DateTime nd1 =
        new DateTime(dateTime.Year, dateTime.Month,
            dateTime.Day, dateTime.Hour,
            dateTime.Minute, dateTime.Second);
    DateTime nd2 =
        new DateTime(dt.Year, dt.Month,
            dt.Day, dt.Hour,
            dt.Minute, dt.Second);
    return nd1 == nd2;
}

Sí probamos nuestro código ahora:

rest4

Ahora sí nos muestra el 304 indicandonos que el recurso no ha sido modificado:

rest5

Y en el body, no viene nada:

rest6

A parte de esta comprobación, tendríamos que utilizar la cabecera HTTP ETag par lograr un buen conditional GET

Por cierto, aquí tenéis el enlace al vídeo del MIX 09 en el Jon habla acerca de la caché en REST y WCF:

http://videos.visitmix.com/MIX09/T64M

Pues esto es todo, espero que os sirva como a mí y no os de quebraderos de cabeza :)

Thanks Jon!!!

Published 22/4/2009 12:36 por Luis Ruiz Pavón
Archivado en: ,
Comparte este post:
http://geeks.ms/blogs/lruiz/archive/2009/04/22/rest-wcf-conditional-get-cuidado-con-la-comprobaci-243-n-de-if-modified-since.aspx

Comentarios

# re: REST WCF - Conditional GET - Cuidado con la comprobación de If-Modified-Since

Buena nota, pena que se me olvidara comentarla a mi :-).. coincido en tu apreciación con Jhon Flanders ..

Saludos

Unai

Wednesday, April 22, 2009 5:39 PM por unai