jueves, 19 de julio de 2012

Arboles de expresión en C# y Linq


Sigo trabajando con Linq y he empezado a meterme con los árboles de expresión. Una definición formal de los árboles de expresión sería que son una estructura de datos que permite expresar código sin compilar, fácil, ¿No?

La definición es un tanto abstracta, así como el concepto de los propios árboles de expresión, pero pueden llegar a ser realmente útiles, especialmente con Linq. La razón por la que comencé a investigarlos fue porque quería modificar las clásulas Where de los objetos IQueryable de Linq, por ejemplo:

        
public void testWhere(IQueryable<person> query, string text)
{
    List<person> list= query.Where(t => t.Nombre.Contains(text)).ToList();           ....
}
        

En este caso realizamos un filtrado de elementos cuya propiedad Name contenga el texto "text", esta es una aplicación práctica muy directa de Linq, redefinimos el where mediante una expresión lambda.

Hasta aquí parece sencillo. El siguiente paso es más complicado, se trata de redefinir la cláusula where, pero sin saber con que objeto estoy tratando. Para hacerlo más abstracto lo que quiero es filtrar por un valor de una propiedad string del objeto (cuyo tipo desconozco) por un texto que le pase. Lo que quiero es un código como este:

Func<t, bool> f = (element) =>
{
    string propertyValue;
    propertyValue = (string) element.[Propiedad];
    string searchText = "[Texto contenedor]";                                
    return  propertyValue.Contains(searchText);
};

El problema es que como el objeto no está tipado, no puedo invocar así de fácil a la propiedad. Para eso necesitamos los árboles de expresión con la cual vamos a definir por entero la función lambda de arriba. Voy a aprovechar para comentar que la cláusula where acepta un predicado de tipo Func<T,bool> siendo T un genérico.

Primer paso: Definimos los parámetros de entrada y las variables intermedias

public static void invocador<T>(T objeto, string propiedad,string search)
{                   
    ParameterExpression element = Expression.Parameter(typeof(T),"element");
            
    ParameterExpression propertyValue = Expression.Variable(typeof(string), "propertyValue");            
    ParameterExpression searchText = Expression.Variable(typeof(string), "searchText");

Hemos definido un parámetro de entrada de tipo T y de nombre element como en la función de arriba. Además hemos definido dos variables intermedias: propertyValue y searchText. Ahora les asignamos valores.

Expression asgPropertyValue = Expression.Assign(propertyValue,
    Expression.Convert(Expression.Property(element,propiedad),typeof(string))
    );

Expression asgSearchText =Expression.Assign(searchText,
    Expression.Constant(search)
    );

En la primera linea le hemos dado valor a la variable propertyValue, utilizando Expression.Assign (operador '=' ), le hemos pasado el valor de una propiedad del objeto tipo T de nombre propiedad (esta propiedad se la pasamos al método arriba), y después la convertimos al tipo String. Es decir:

propertyValue = (string) element.[Propiedad];


En la segunda linea le hemos asignado a la variable searchText el valor constante search (que recibimos por parámetro), es decir:

string searchText = "[Texto contenedor]";   

Ahora tenemos que llamar al método Contains de la clase String:

Expression callContains = Expression.Call(propertyValue, typeof(String).GetMethod("Contains"), searchText);

Aquí hemos invocado al método Contains de la variable propertyValue, pasándole por parámetro la variable searchText, finalmente tenemos que crear un bloque de instrucciones para que se ejecuten en el orden adecuado:


Expression block = Expression.Block(typeof(bool), 
    new List {propertyValue, searchText},                                    
    asgPropertyValue,
    asgSearchText,
    callContains                                                                        
);

Primero se pasan las variables y luego las instrucciones en los bloques de expresión. Ya casi lo tenemos, solo nos falta generar la función lambda y utilizarla :)



Expression<Func<T, bool>> exprFunc = Expression.Lambda<Func<T, bool>>(block, element);
                                 
var func = exprFunc.Compile();

System.Console.WriteLine(func(objeto));
Os pongo todo el código junto y un ejemplo de uso:
    public class Person
    {
        private string nombre;

        public String Nombre
        {
            get { return nombre; }
            set { nombre = value; }
        }

        public Person(string nombre)
        {
            this.nombre = nombre;
        }
    }

    public class Program
    {        
        public static void Main(string[] args)
        {         
            Person javi = new Person("Javi");
            invocador<Person>(javi, "Nombre", "avi");
            invocador<Person>(javi, "Nombre", "ene");        
            Console.ReadKey();            
        }

       
        public static void invokePrint(Func<int, int, int> caller, int param1, int param2)
        {
            int result = caller(param1, param2);
            Console.WriteLine(result);
        }

      
        public static void invocador<T>(T objeto, string propiedad,string search)
        {                   
            ParameterExpression element = Expression.Parameter(typeof(T),"element");            
            ParameterExpression propertyValue = Expression.Variable(typeof(string), "propertyValue");            
            ParameterExpression searchText = Expression.Variable(typeof(string), "searchText");
            Expression asgPropertyValue = Expression.Assign(propertyValue,
                Expression.Convert(Expression.Property(element,propiedad),typeof(string))
                );

            Expression asgSearchText =Expression.Assign(searchText,
                Expression.Constant(search)
                );

            Expression callContains = Expression.Call(propertyValue, typeof(String).GetMethod("Contains"), searchText);         


            Expression block = Expression.Block(typeof(bool), 
                new List<ParameterExpression> {propertyValue, searchText},                                    
                asgPropertyValue,
                asgSearchText,
                callContains                                                                        
            );

            Expression<Func<T, bool>> exprFunc = Expression.Lambda<Func<T, bool>>(block, element);
                                 
            var func = exprFunc.Compile();

            System.Console.WriteLine(func(objeto));
        }
    }

Espero que os haya sido de ayuda a pesar de que es un tema un tanto complejo.

No hay comentarios:

Publicar un comentario