Desacoplando System.Web.Cache de nuestra lógica de negocio

Normalmente la tarea que más aumenta el tiempo de respuesta en la devolución de una página ASP.NET que consulta contra una BD es la propia consulta a la BD. Un escenario frecuente es la recuperación y presentación de datos con poca tasa de actualización. Para cada cliente que ejecuta la página se ejecuta de nuevo la consulta. Pese a que internamente SQL Server u otro gestor SGBD suele tener planificada las últimas consultas SQL realizadas y dispone de sus propios cachés, no tenemos la certeza de que así sea, sobre todo en momentos de alta carga de consultas, justo cuando más se necesita.


La primera aproximación para mejorar el throughput contra la BD es hacer las consultas solo en la primera petición de la página, en post backs sucesivos no se realiza la consulta.


    protected void Page_Load(object sender, EventArgs e)


    {


        if (!Page.IsPostBack)


        {


            BindData();


        }


    }


El método BindData() obtendría los datos de la BD y, por ejemplo, se los asociaría a un GridView , en peticiones posteriores  de la página por parte del mismo cliente no se ejecutaría este método, pero se seguirían mostrando los datos en el viewstate.  Este codelet significó una gran mejora de ASP.NET 1.0 frente ASP. En ASP.NET 1.0 se introdujo ya un conjunto de clases para gestionar un caché de datos y su política de invalidación. ASP.NET 2.0 mejora y extiende estas características.


Caché de datos global


Supongamos un escenario en el que tenemos una serie de datos en la BD relativamente inmutables y que utilizan en común todos los clientes de nuestra aplicación ASP.NET 2.0. Por ejemplo, una tabla de provincias o de empleados. Con la técnica del apartado anterior, cada vez que un nuevo cliente solicita o recarga la página que contiene un combo con la lista de provincias se ejecuta al menos una consulta contra la BD.


Puesto que los datos son comunes a todos los clientes de la aplicación podemos usar el caché de datos global. Este caché, a menos que se especifique lo contrario, tiene el mismo tiempo de vida y validez que el proceso de nuestra aplicación ASP.NET en el servidor. La afinidad del caché es a nivel de aplicación en un pool de IIS. Por lo que en un entorno de webfarming se tendría un caché global para cada aplicación en un pool dentro de un nodo del clúster.


El caché de ASP.NET en una arquitectura de tres capas


En los ejemplos que se pueden encontrar en la documentación de ASP.Net o en los tutoriales de www.asp.net se muestran ejemplos del uso del caché con datos en aplicaciones de una sola capa; dentro de una misma página ASP.NET se realizan las consultas a datos, su procesamiento y visualización.


Si bien es práctico en desarrollos de poco alcance, cuando se va a desarrollar una aplicación en un entorno empresarial es muy recomendable una arquitectura en al menos tres capas; Interfaz de usuario (UI), Lógica de Negocio (Business Logic, BL) y acceso a datos (Data Access, DA).


Si queremos emplear el caché global de datos en nuestra lógica de negocio observaremos que depende realmente de clases del interfaz de usuario pues el objeto Cache se obtiene o bien del objeto Page actual o bien del objeto HttpContext. No podemos usar estas clases en nuestra Lógica de Negocio porque se haría dependiente del interfaz empleado.  La capa BL debería servirnos para cualquier tipo de interface, WindowsForms, aplicación de consola, etc.


Para desacoplar las clases de caché dependientes de ASP.NET de la lógica de negocio, os propongo el siguiente diseño, basado en el patrón de diseño Estrategia (Strategy Pattern) [GAM97] y un patrón Adaptador  (wrapper) [GAM97] . Permite elegir varias estrategias de cache y el segundo abstrae el interfaz de System.Web.Caching.Cache .  Para el acceso a datos del modelo conceptual se utilizan DataSets en vez de objetos DTO junto a una clase DAO (Direct Access Object) que realiza las consultas al gestor  SqlServer.


El interfaz de usuario (UI) accede a la lógica de negocio a través de una fachada (Facade Pattern)[GAM97] que al carecer de estado es una clase de instancia única (Singleton Pattern)[GAM97]


 Diagrama 1. Diagrama estático de clases del ejemplo Sample.TestCache


Diagrama 1. Diagrama estático de clases del ejemplo Sample.TestCache


La página por defecto invoca con su método BindData() el enlace inicial de los datos a los controles visuales.


        private void BindData()


        {


            FacadeData facade = FacadeData.getWithCacheStrategy();           


            GridView1.DataSource = facade.Employees;


            GridView1.DataBind();


        }


En el fragmento de código superior vemos cómo invocando el método getWithCacheSrtategy() se obtiene de forma implícita una instancia de la fachada de acceso a la lógica de negocio y se solicita que utilice una estrategia de caché. La implementación concreta de ICacheStrategy se establece en el fichero web.config.


  <appSettings>


    <add key=CacheStrategyClass value=Samples.TestCache.CacheStrategyAspNet, Samples.TestCache/>   


  </appSettings>


La fachada tiene otros dos métodos para obtener una instancia de si misma;  getInstance(), que carece de estrategias de cache y getWithCacheStrategy(ICacheStrategy) que acepta desde código una estrategia de cache explícita.


 


        public static FacadeData getWithCacheStrategy(ICacheStrategy ipd)


        {


            if (instance == null)


                instance = new FacadeData();


            CacheStrategy = ipd;


            return instance;


        }


 


        public static FacadeData getWithCacheStrategy()


        {


            return getWithCacheStrategy(getInstanceCacheStrategy());


        }


        private static ICacheStrategy getInstanceCacheStrategy()


        {


            string strCacheStrategyClass =


                   WebConfigurationManager.AppSettings[“CacheStrategyClass”];


           


            if (strCacheStrategyClass != null)


                return (ICacheStrategy) Activator.CreateInstance(


                                            Type.GetType(strCacheStrategyClass));                     


            else


                return null;


        }


La fachada también actúa en como proxy a la hora de recuperar los empleados. Se hace bajo demanda y devuelve el dataset del caché en caso de que así se haya configurado o directamente del DAO.


        public DataSet Employees


        {


            get


            {


                if (CacheStrategy != null)


                {


                    if (CacheStrategy.getData(“cachedEmployees”) == null)


                        CacheStrategy.setData(“cachedEmployees”, EmployeeDAO.getEmployees())


                    


   return CacheStrategy.getData(“cachedEmployees”);


                }


                else


                    return EmployeeDAO.getEmployees();


            }


        }


El interface de ICacheStrategy para este ejemplo, es muy sencillo:


    public interface ICacheStrategy


    {


       DataSet getData(string dataKey);


       void setData(string dataKey, DataSet data);


    }


La implementación para el caso de utilizar el caché de ASP.NET:


        public DataSet getData(string dataKey)


        {


            HttpContext context = System.Web.HttpContext.Current;


            return (DataSet) context.Cache[dataKey];


        }


 


        public void setData(string dataKey, DataSet data)


        {


            HttpContext context = System.Web.HttpContext.Current;


            context.Cache.Remove(dataKey);


            context.Cache.Add(dataKey, data, null, DateTime.Now.AddMinutes(MIN_TO_EXPIRE),


                   Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);


        }


El método setData podría haberse simplificado, sin embargo he preferido esta opción porque, entre otras cosas, se puede controlar el tiempo de invalidación del caché y su prioridad. La clase precisa del contexto HTTP para poder acceder al caché.


Si distribuímos en ensamblados las clases, en el del IU tendríamos las clases Default (página ASPX). En el ensamblado de la lógica de negocio estarían el resto de clases. Nótese que CacheStrategyAspNet tiene una dependencia indirecta a HttpContext.


Este ejemplo podría haberse realizado igualmente con objetos del modelo en vez de con DataSets.


Si ejecutamos la aplicación veremos que múltiples peticiones devuelven los mismos datos obtenidos en el mismo instante (columna  RetrievedAt). Incluso cuando el cliente recarga la página.  Solo cuando hace clic en “Refresh Data” se actualiza el caché.  Recordad que este caché es privado a la aplicación en ejecución, se comparte entre todas las sesiones que mantenga la aplicación dentro del pool del servidor IIS, si por ejemplo queréis hacer una consulta con parámetros dependientes de la sesión este esquema no os valdría.


Con este ejemplo hemos visto cómo desacoplar la tecnología de caché de asp.net de una fachada de nuestra lógica de negocio.


Queda abierta la discusión para el caso de querer crear un cliente ligero y llevar la lógica de negocio a un servidor distinto al de IIS, usando por ejemplo, Windows Communication Foundation.


Referencias


 [GAM97]            Design Patterns, Elements of Reusable Object-Oriented Software, Gamma et Al.  Addison-Wesley Eds. 1997. ISBN 0-201-63361-2


3 comentarios en “Desacoplando System.Web.Cache de nuestra lógica de negocio”

  1. Hola Eduardo:

    De hecho la caché de ASP.NET sí que se puede usar sin problemas en contextos diferentes a la Web.

    Basta con instanaciarla a decuadamente ya que toda la funcionalidad está incluida por separado en el espacio de nombres System.Web.Caching. Lo difícil es instanciar la clase Cache de este espacio de nombres.

    En concreto la forma más fácil de hacerlo es instanciando la clase HttpRuntime y usando su propiedad Cache. Por ejemplo en una aplicación de consola se haría así (hay que agregar una referencia a System.Web.dll):

    using System.Web;
    using System.Web.Caching;

    public class Prueba
    {
    public static void Main()
    {
    HttpRuntime httpRT = new HttpRuntime();
    Cache cache = HttpRuntime.Cache;
    cache[“prueba”] = “Hola Edu”;
    Console.ReadLine();
    Console.WriteLine(cache[“prueba”]);
    Console.ReadLine();
    }
    }

    En una DLL se haría igual.

    Esta caché se mantiene todo el rato para un mismo dominio de aplicación, así que nos vale para cualquier aplicación de host que queramos crear (por ejemplo en una capa de negocio remota) o para un EXE normal mientras se esté ejecutando, por lo que tiene una gran utilidad.

    En cualquier caso me ha parecido muy interesante todo el desarrollo de este post, así como la arquitectura del patrón que has elegido.

    Enhorabuena 🙂

    Si no te importa a lo mejor pongo este comentario en mi blog, pues me parece que le puede interesar a sus visitantes.

    Un saludo

    JM.

  2. Gracias Jose!

    Efectivamente ya había leído un artículo por ahí respecto a a utilizar el caché de ASP.NET en aplicaciones de consola y windows.forms, de todas formas lo que trataba era de desacoplar la tecnología de caché de nuestra aplicación, si por ejemplo en un futuro quisieramos utilizar un simple Dictionary solo tendríamos que implementar la Interface ICacheStrategy y cambiar el App.Config o Web.Config…

    Ah, por si quieres completar el comentario, he leido también que al parecer en aplicaciones windows.forms hay problemas en con el caché ASP.NET si accedes a el desde distintos threads en periodos breves de tiempo.

    Edu.

  3. Hola Eduardo,

    hace mucho tiempo de esta entrada y ya no me acordaba de ella cuando hoy escribí algo relacionado con System.Web.Caching.Cache.

    Sirva como elemento de referencia, que lo pongo aquí en el comentario para enlazar con más información y en este caso, con el uso de ese ensamblado en ambientes Windows (ojo, no es oro todo lo que reluce). 🙂

    http://geeks.ms/blogs/jorge/archive/2009/02/21/uso-de-caching-en-windows-utilizando-system-web-caching.aspx

Deja un comentario

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