martes, 22 de febrero de 2011

[ASP.NET] Lockear Edición de Entidades

 

Introducción

La finalidad del articulo será demostrar como implementar lockeos pesimistas usando las características que un entorno web puede brindar.

Seguramente será bien conocido el lockeo por medio de la base de datos, definiendo un campo del tipo TimeSpan y ante una actualización validar si este ha cambiado, si lo hizo se informa al usuario y se evita la operación hasta resolver el conflicto. Este se lo denomina lockeo optimista ya que no evita que cada usuario edite el registro, pero lo valida al momento de realizar la transacción.

Ahora bien como implementar aprovechando las características de un entorno web un lockeo de entidad distinto

Primero mencionaremos porque un entorno web provee ciertas características que hacen posible este tipo de implementación

- se dispone de un ambiente centralizado, todos lo usuario debes consultar el servidor para poder editar las entidades

- distintas sesiones se pueden comunicar y mantener información global a nivel del sitio por medio de la declaración de variables estáticas, usar esto o el objeto Application de asp.net es semejando ya que se comportan de la misma forma

Locker Helper

El núcleo por el cual el lockeo es implementado se desarrolla en una clase muy simple

public static class LockHelper<TEntity> where TEntity : class
{
    private static Dictionary<string, List<LockInfo>> lockList = new Dictionary<string,List<LockInfo>>();

    public static LockResult Lock(int idEntity, string userName)
    {
        LockInfo newLock = null;

        if (!lockList.ContainsKey(typeof(TEntity).Name))
        {
            newLock = new LockInfo()
            {
                UserName = userName,
                IdEntity = idEntity
            };

            lock (lockList)
            {
                lockList.Add(typeof(TEntity).Name, new List<LockInfo>() { newLock });
            }
            
            
            return new LockResult()
                {
                    Result = LockResultEnum.Succesully,
                    LockInfo = newLock
                };
        }

        //
        // Se valida sino estaba previamente lockeado por alguien mas
        //
        LockInfo alreadyLockedResult = lockList[typeof(TEntity).Name]
                                            .FirstOrDefault(o => o.IdEntity == idEntity
                                                                    && o.UserName != userName);

        if (alreadyLockedResult != null)
            return new LockResult()
            {
                Result = LockResultEnum.AlreadyLocked,
                LockInfo = alreadyLockedResult
            };

        //
        // Si se intenta lockear algo que ya estaba previamente lockeado solo se devuelve
        // el LockInfo que se tenia
        //
        LockInfo lockResult = lockList[typeof(TEntity).Name]
                                    .FirstOrDefault(o => o.IdEntity == idEntity 
                                                            && o.UserName == userName);
        if (lockResult != null)
            return new LockResult()
                        {
                            Result = LockResultEnum.Succesully,
                            LockInfo = lockResult
                        }; 


        newLock = new LockInfo()
        {
            UserName = userName,
            IdEntity = idEntity
        };

        lock (lockList)
        {
            lockList[typeof(TEntity).Name].Add(newLock);
        }

        return new LockResult()
                {
                    Result = LockResultEnum.Succesully,
                    LockInfo = newLock
                };
    }

    public static void UnLock(int idEntity)
    {
        lock (lockList)
        {
            lockList[typeof(TEntity).Name].RemoveAll(o => o.IdEntity == idEntity);
        }
    }


}


public class LockInfo
{
    public string UserName { get; set; }
    public int IdEntity { get; set; }
}

public class LockResult
{
    public LockResultEnum Result { get; set; }
    public LockInfo LockInfo { get; set; }
}

public enum LockResultEnum
{
    AlreadyLocked = 0,
    Succesully = 1
}

Esta básicamente esta compuesta por método estáticos y particularmente es la que define el

private static Dictionary<string, List<LockInfo>> lockList

esta linea aunque parece insignificante es donde se contendrá la información de lockeos para todos los usuario que trabajen con la aplicación, el helper básicamente realizara consulta, agregara y quitara elementos en esta lista.

Como comente anteriormente ls lista esta definida con static para que esta actué como si fuera el objeto Application y este disponible a nivel del sitio, o sea pueda ser accedida por todos las sesiones y request que se realicen, seria como un repositorio en memoria.

 

Como usarlo ?

Esta es la gran pregunta, como hacer uso de la funcionalidad implementada.

 

        protected void gvCourse_RowEditing(object sender, GridViewEditEventArgs e)
        {
            lblMessage.Text = string.Empty;

            //
            //Obtengo el id de la entidad que se esta editando
            //
            int id = Convert.ToInt32(gvCourse.DataKeys[e.NewEditIndex].Value);

            LockResult resultLock = LockHelper<Course>.Lock(id, Thread.CurrentPrincipal.Identity.Name);

            if (resultLock.Result == LockResultEnum.Succesully)
            {
                gvCourse.EditIndex = e.NewEditIndex;

                DataBindGridView();
            }
            else
                lblMessage.Text = string.Format("El Curso {0} esta siendo editado por: {1}", id, resultLock.LockInfo.UserName);

        }

        protected void gvCourse_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e)
        {
            int id = Convert.ToInt32(gvCourse.DataKeys[e.RowIndex].Value);

            LockHelper<Course>.UnLock(id);

            gvCourse.EditIndex = -1;
            DataBindGridView();
        }

Bien, como se observa es en la edición de registros de un GridView donde se realiza la operación de lockeo, al metodo Lock() se le proporciona el id de la entidad que se quiere bloquear y el usuario que solicita esta acción.

Si todo va bien se edita el registro, pero puede ocurrir que este tomada la edición por otro usuario, en tal caso el método devolverá en su respuesta el estado que lo indica y es usado para notificar al usuario.

Codigo de Ejemplo

El código de ejemplo cuanta con una base de datos creada en Sql Server 2008, pero en caso de tener problemas en el uso podrían ejecutarse los script que acompañan al ejemplo.

Para autenticarse al ejecutar el ejemplo solo debe ingresar correctamente el nombre del usuario, ya que aplica una validación simple sin verificar el password, en este puede ingresarse cualquier valor

Como nombres de usuario podrías elegir: Alonso, Suarez, Lopez, cualquiera de estos serán validos, pero si se quiere se puede consultar la tabla Person allí esta el resto.

 

[C#]
 

4 comentarios:

  1. hola como estas leandro, tengo una duda con respecto a los Stored Procedure, veo que tu en tus ejemplos no los sueles utilizar te pregunto el motivo es porque justamente se debe ah que son para ejemplo simples y demostracion pero en un caso real en un proyectos se debe de usar de todas maneras, en que situaciones se debe de Usar StoredProcedure y cuando no?, cuales son sus ventjas de utilizarlas, etc, me gustaria que me des tu respuesta en base a tu experiencia pues mucho gusto y exitos.

    ResponderEliminar
  2. hola luis fernando

    Exacto es como lo has mencionado, aqui no uso SP porque quiero reflejar en el codigo la consulta que se utiliza en un solo vistazo y se aprecien el uso de parametros y demas temas que por ahi con un SP seria mas dificil dde mostrar, pero si puede usar SP, usalos.

    Desventajas del uso la verdad no tendrias, es mas hay ventajas como ser que la query ya esta compilada dentro de la db y esta trazo un plan de eejcucion para optimizar tiempos en la respuesta, ademas evitas el parseo previo de la query.


    saludos

    ResponderEliminar
  3. Hola Leandro.

    Me parece muy interesante el articulo, y acudo solicitando ayuda respecto al tema.
    ¿Cómo se controlaría el deslockeo cuando el usuario cierre el navegador, o no termine la edición?del registro. ¿Hay alguna manera de deslockear los registros luego de determinado tiempo?
    He leído algo acerca de llamar a una función con ajax, pero la verdad no se como hacerlo.

    Mil gracias por el tiempo y la ayuda que me pueda brindar

    ResponderEliminar
  4. hola Alex

    esa parte me falto incluirla en el articulo

    basicamente usarias el

    window.onbeforeunload

    para detectar el cierre del browser

    y despues con la ayuda de jquery, concretamente con $.ajax invocarias un webmethod para deslokear

    en este otro articulo
    [jQuery] DropDownList anidados (nested DropDownList)

    utilizo el $.ajax con webmethod para que veas como implementarlo

    saludos

    ResponderEliminar