domingo, 26 de octubre de 2014

OutputCache con MvcDonutCaching y MongoDB

Si hablamos de cache en una aplicación ASP.NET MVC, una de las primeras opciones a valorar es utilizar el atributo OutputCache para cachear la salida de los controladores.

Aunque a priori OutputCache podría satisfacer nuestros requerimientos, lo cierto es que adolece de algunos problemas que hacen que uso no sea todo lo satisfactorio que cabría esperar.

Muchos de estos problemas están relacionados con el uso del atributo en acciones hijas (las que son invocadas con Html.Action). En concreto, encontramos las siguientes limitaciones cuando hablamos de acciones hijas:

  • Sólo soportan las propiedades Duration, VarByCustom y VarByParam. Lo más relevante aquí es que no soporta la propiedad CacheProfile y, por ende, no podemos establecer una política de expiración de caché a través del web.config.
  • Ignora por completo el valor del atributo enableOutputCache de la sección outputCache del web.config. Esto significa que algo tan habitual como jugar con este valor para activar o desactivar la cache en desarrollo, no funcionará con acciones hijas.

En cualquier caso, no sólo las acciones hijas presentan limitaciones. Si pensamos en qué querríamos cachear en el contexto de la salida de ASP.NET MVC, encontraríamos fácilmente los siguientes escenarios:

  • Cachear la página entera (Full page caching). Soportado puesto que cachear una acción padre engloba cachear todo el resultado del método de acción.
  • Cachear pequeños fragmentos de la página (también llamado cachear el agujero del donut, Donut hole caching). Igualmente soportado a través de la cache de acciones hijas.
  • Cachear la página entera excepto pequeños fragmentos (lo que se llama cachear el donut pero no el agujero, Donut caching). Esto NO está soportado por el proveedor de serie aun cuando, si recordamos, se podía hacer por ejemplo en WebForms a través del control Substitution.

Otro problema grave que nos encontramos es que invalidar la cache no es algo que esté muy conseguido. Sólo tenemos disponible un triste método Response.RemoveOutputCacheItem(path) que sólo funciona para acciones padre y además no permite invalidar de una sola vez todas las versiones de una página, es decir, tendremos que llamar a este método tantas veces como distintas versiones de path haya que incluyan parámetros como segmentos en el path.

El cómo solventar todos estos problemas de un plumazo es sencillo, utilizando el paquete MvcDonutCaching

Este paquete nos brinda un nuevo atributo llamado DonutOutputCache, que sustituye por completo a OutputCache y hace que todo funcione como debería funcionar. Ahora no hay distinción entre acciones padres e hijas, todo funciona (las acciones hijas ya pueden utilizar CacheProfile, están al tanto de enableOutputCache, etc.) y además nos permite cachear el donut (Donut caching). En este sentido, MvcDonutCaching lo pone muy fácil porque agrega sobrecargas al helper Html.Action para poder especificar si la llamada a la acción hija debe excluirse de la cache de su padre, por ejemplo en vez de escribir @Html.Action(“Acción”), si escribimos @Html.Action(“Accion”, true) ya estaríamos cacheando el donut.

MvcDonutCaching además también permite invalidar la caché de forma precisa con los métodos RemoveItem y RemoveItems de la clase OutputCacheManager.

En este punto ya somos más felices, pero si cabe (y si es necesario) podríamos serlo un poco más. Esto es debido a que a partir de .NET 4.0 se introdujo la posibilidad de crear nuestro propio proveedor de cache personalizado y configurarlo vía web.config. Siendo así podemos hacer que nuestra cache persista en un almacenamiento compartido permitiendo que varios frontales compartan la misma cache e incluso que la cache sobreviva a reinicios de la aplicación. En mi caso he optado por crear un proveedor de cache basado en MongoDB.

Para ello hay que crear una clase que herede de OutputCacheProvider, implemente los métodos necesarios y configurar el nuevo proveedor vía web.config.

El código del proveedor está disponible en github https://github.com/panicoenlaxbox/MvcDonutCaching.MongoDBOutputCache y también hay disponible paquete de Nuget para instalarlo de forma sencilla http://www.nuget.org/packages/MvcDonutCaching.MongoDBOutputCache/

Resumiendo, los pasos para configurar el cache como hemos explicado son:

1. Instalar el paquete MvcDonutCaching

2. Instalar el paquete MvcDonutCaching.MongoDBOutputCache

3. Configurar el proveedor en el web.config

    <caching>

      <outputCache defaultProvider="AspNetMongoDB">

        <providers>

          <add name="AspNetMongoDB" type="MongoDBOutputCache.MongoDBOutputCacheProvider, MongoDBOutputCache" />

        </providers>

      </outputCache>

    </caching>

4. Agregar las siguientes claves en AppSettings (lógicamente apuntando a la instancia de MongoDB que quieras utilizar – con o sin autenticación – y especificando la base de datos que quieres utilizar):

    <add key="MongoDBOutputCacheProviderConnectionString" value="mongodb://admin:admin@localhost/aspnet" />

    <add key="MongoDBOutputCacheProviderCollection" value="OutputCache" />

Y ahora sí, tenemos una cache de salida de ASP.NET MVC como hubiésemos querido en un principio!

Un saludo!

jueves, 16 de octubre de 2014

Conversiones de usuario en C#

Las conversiones de usuario permiten definir conversiones personalizadas en C#, tanto explícitas como implícitas, desde un tipo origen a un tipo destino.

Lo cierto es que no suelo utilizarlas mucho (por no decir rara y extrañísima vez), pero como siempre estamos buscando una oportunidad para poner en juego características del lenguaje, recientemente me he encontrado con un caso que he resuelto con una conversión de usuario.

Para poner en situación, primero veremos un ejemplo de cómo son y qué hacen.
using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var age = 38;
            Person person = age; // implícita de int a Person
            Console.WriteLine(person.Age); // 38 
            age = (int)person; // explícita, necesario cast, de Person a int            
            Console.WriteLine(age); // 38
            Console.ReadKey();
        }
    }

    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public static implicit operator Person(int age)
        {
            return new Person() { Age = age };
        }

        public static explicit operator int(Person person)
        {
            return person.Age;
        }
    }
}
Como se puede ver en el ejemplo, la sintaxis de una conversión de usuario es:
public static {implicit|explicit} operator TipoDestino (TipoOrigen identificador) 
{
 // do something…
return TipoDestino;
}
Las restricciones que se aplican a una conversión de usuario son:

  • Sólo son válidas para clases y estructuras.
  • TipoDestino y TipoOrigen tienen que ser de distinto tipo.
  • No puede haber una relación de herencia entre TipoDestino y TipoOrigen
  • TipoDestino y TipoOrigen no pueden ser de un tipo interface ni object.
  • No se puede declarar la misma conversión tanto de forma implícita como explícita
  • No se puede usar el operador is
  • No se puede usar el operador as

Después de la teoría y ya sabiendo que es una conversión de usuario, el ejemplo donde he aplicado una conversión de usuario es en un método que devuelve si se puede o no enviar un pedido. El problema está en que si no se puede enviar el pedido, querría saber también el motivo. Es decir, el siguiente código no me servía.
    public class Order
    {
        public string CustomerId { get; set; }
        public DateTime? DeliveryDate { get; set; }

        public bool CanBeSent()
        {
            if (string.IsNullOrEmpty(CustomerId))
            {
                return false;
            }
            if (DeliveryDate == null)
            {
                return false;
            }
            return true;
        }
    }
Para solucionar el problema expuesto y haciendo uso de las conversiones de usuario, podemos hacer que el método devuelve un tipo complejo indicando si es o no posible enviar el pedido y, en caso negativo, también especificar el motivo. Además, el valor devuelto se puede trabajar tanto como un booleano como un tipo complejo cuando sea necesario.
using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var order = new Order();
            var result = order.CanBeSent();
            if (result)
            {
                Console.WriteLine("Se puede enviar el pedido");
            }
            else
            {
                Console.WriteLine("No se puede enviar el pedido");
                Console.WriteLine(result.Reason);
            }
            Console.ReadKey();
        }
    }

    public class CanBeResult
    {
        public bool Value { get; set; }
        public string Reason { get; set; }

        public CanBeResult(bool value)
        {
            Value = value;
        }

        public CanBeResult(bool value, string reason)
            : this(value)
        {
            Reason = reason;
        }

        public static implicit operator bool(CanBeResult result)
        {
            return result.Value;
        }

        public static implicit operator CanBeResult(bool value)
        {
            return new CanBeResult(value);
        }
    }

    public class Order
    {
        public string CustomerId { get; set; }
        public DateTime? DeliveryDate { get; set; }

        public CanBeResult CanBeSent()
        {
            if (string.IsNullOrEmpty(CustomerId))
            {
                return new CanBeResult(false, "El pedido no tiene cliente");
            }
            if (DeliveryDate == null)
            {
                return new CanBeResult(false, "El pedido no tiene fecha de entrega");
            }
            return true;
        }
    }
}
Un saludo!