Introducción A partir de la versión 4.5 de .Net framework se ha simplificado de forma considerable la forma en que podemos trabajar con código asíncrono. Con los anteriores frameworks, si queríamos contar con los beneficios de una programación asíncrona nos veíamos obligados a lidiar con una gran complejidad en nuestro código. Esto nos hacía evitar su uso en lo posible a pesar de perder esta importante característica. Stephen Cleary , un MVP especializado en concurrencia define la programación asíncrona como: Una forma de concurrencia que utiliza futuros o "callbacks" para evitar hilos innecesarios. La programación moderna con async y await nos abstrae de la utilización de "callbacks" y nos permite utilizar futuros (Tasks) que se encargarán de notificar al llamante cuando el método asíncrono se complete. async y await Las palabras async y await son las palabras clave que .Net ha introducido en el lenguaje para que podamos implementar métodos asíncronos co...
Arboles de expresión
Un árbol de expresión es una expresión lambda sin compilar. Cuando utilizamos un árbol de expresión, el código de la expresión lambda se mantiene en memoria en forma de estructura de árbol binario. Se almacenan las partes de la expresión y las operaciones entre las partes por separado en una estructura de árbol. En este estructura los nodos representan operadores y las hojas representan valores. De esta forma es posible representar cualquier operación como una jerarquía. Veamos un ejemplo de árbol de expresión binario muy simple:En el ejemplo, si partimos de las hojas hacia la raíz nos encontramos primero con los valores 20 y 5 cuyo nodo común es el operador suma, por lo que el operador podría sustituirse por el valor resultado de la suma: 25.
A continuación tendríamos dos hojas con los valores 25 y 10 con un nodo común con la operación multiplicar, por lo que el resultado de ambos será de 25 multiplicado por 10: 250. Al ser la raíz del árbol este sería el valor del resultado de la expresión.
Las expresiones suelen ser mucho más complejas que en el ejemplo y en vez de valores constantes podemos tener variables y llamadas a métodos. También tendremos en el árbol una rama que representará los parámetros de entrada de la expresión.
Veamos ahora un ejemplo de una expresión lambda y su árbol de expresión equivalente. Como se puede observar, para invocar la función del árbol de expresión es necesario compilarla primero para que devuelva su delegado equivalente. El resultado de la invocación es el mismo en ambos casos:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Cliente cliente = new Cliente { VolumenVentas = 30000 }; | |
// Expresión lambda | |
Func<Cliente, bool> EsVip = c => c.VolumenVentas > 20000; | |
bool esVip = EsVip.Invoke(cliente); | |
// Expresión de árbol | |
Expression<Func<Cliente, bool>> EsVipExpression = c => c.VolumenVentas > 20000; | |
esVip = EsVipExpression.Compile().Invoke(cliente); |
Es muy habitual que un método nos solicite un árbol de expresión como parámetro y que la expresión nunca sea compilada ni ejecutada. En estos casos el parámetro tiene el único propósito de informar en detalle cómo está estructurada la función. Más adelante veremos algunos ejemplos donde se entenderá mejor este aspecto.
Reflexión estática
Un árbol de expresión tiene la ventaja de poder ser analizada miembro a miembro utilizando la reflexión estática. Veamos cómo convierte el compilador un árbol de expresión en una estructura de datos.En el siguiente ejemplo se muestra un árbol de expresión y a continuación la forma en que el compilador la descompondrá en miembros de una estructura. En realidad las dos formas son válidas para declarar un árbol de expresión, sin embargo, la forma más rápida e intuitiva es la primera donde se utiliza la sintaxis de expresión lambda: [parámetros] => [cuerpo de la expresión]. La segunda forma nos ofrecería la capacidad de "montar al vuelo" una función con estructura de árbol binario, permitiendo crear métodos dinámicos de una forma muy simple.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Expression<Func<Clientes, bool>> EsVip = c => c.Volumen > 20000; | |
// param. Parametro de entrada (Clientes). Lado a la izquierda del operador '=>' | |
ParameterExpression param = Expression.Parameter(typeof(Clientes), "cliente"); | |
// body. Cuerpo de la expresión (c.Volumen > 20000). Lado a la derecha del operador '=>' | |
MemberExpression member = Expression.Property(param, "Volumen"); // c.Volumen | |
ConstantExpression cte = Expression.Constant(20000m, typeof(decimal)); // 20000 | |
BinaryExpression body = Expression.GreaterThan(member, cte); // member > 20000 | |
// Espresión completa. body, param | |
Expression.Lambda<Func<Clientes, bool>>(body, new[] { param }); |
De forma inversa, a partir de un árbol de expresión, podemos descomponerla en sus miembros individuales y analizar cada miembro por separado.
A esta forma de análisis, muy similar a la reflexión dinámica que utilizamos para el análisis de tipos, se le llama reflexión estática. El siguiente código realiza una simple inspección de la expresión
EsVip
utilizada en el ejemplo anterior:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var parameters = EsVip.Parameters; | |
// Expression | |
Console.WriteLine("Expression: {0}", EsVip); | |
Console.WriteLine("Tipo expression: {0}", EsVip.NodeType); | |
Console.WriteLine("Tipo de volor devulto: {0}", EsVip.ReturnType); | |
// Parámetro [0] | |
Console.WriteLine("Nombre parámetro: {0}", parameters[0].Name); | |
Console.WriteLine("Tipo parámetro: {0}", parameters[0].Type.Name); | |
// Body | |
var bodyExpr = EsVip.Body as BinaryExpression; | |
Console.WriteLine("Expresión izda: {0}", bodyExpr.Left); | |
Console.WriteLine("Expresión dcha: {0}", bodyExpr.Right); | |
Console.WriteLine("Tipo de nodo: {0}", bodyExpr.NodeType); |
Tipo expression: Lambda
Tipo de volor devulto: System.Boolean
Nombre parámetro: c
Tipo parámetro: Clientes
Expresión izda: c.Volumen
Expresión dcha: 20000
Tipo de nodo: GreaterThan_
Ventajas y usos de la reflexión estática
La reflexión estática recopila información inspeccionando un árbol de expresión. La reflexión dinámica permite obtener información de los ensamblados y los tipos definidos dentro de ellos.La principal ventaja que ofrece la reflexión estática es que utiliza código tipado. De esta forma nos aseguramos tener siempre un código válido. Por el contrario, la reflexión dinámica utiliza cadenas de texto "magic strings" que dan pie a errores de tipografía. Además, la reflexión estática permite utilizar Intellisense de Visual Studio para mayor productividad y comodidad así como realizar refactorizaciones automáticas sin preocuparnos de romper el código.
Examinemos los dos tipos de reflexión con el siguiente ejemplo que simplemente comprueba si una propiedad está marcada o no con el atributo
ObsoleteAttribute
:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
PropertyHelper helper = new PropertyHelper(); | |
// Reflexión dinámica | |
bool esObsoleto = helper.PropiedaEsObsoleta<Clientes>("Nombre"); | |
// Reflexión estática | |
esObsoleto = helper.PropiedaEsObsoleta(c => c.Nombre); |
string
, no tiene información relativa al objeto sobre el que queremos examinar la propiedad. Podemos solucionarlo añadiendo un parámetro más con el objeto a examinar o, tal y como hemos hecho en el ejemplo, utilizando un método genérico donde el tipo genérico establece el tipo de objeto.El segundo método acepta un árbol de expresión, ofreciendo un tipado seguro. En este caso estamos pasando en un mismo parámetro toda la información necesaria para examinar la propiedad.
Este sería el código que podría usarse dentro de cada uno de los métodos anteriores:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class PropertyHelper | |
{ | |
public bool PropiedaEsObsoleta<TModel>(string nombrePropiedad) | |
{ | |
PropertyInfo prop = typeof(TModel).GetProperty(nombrePropiedad); | |
var attr = prop.GetCustomAttribute<ObsoleteAttribute>(); | |
return attr != null; | |
} | |
public bool PropiedaEsObsoleta(Expression<Func<Clientes, object>> expresionPropiedad) | |
{ | |
var bodyExpr = expresionPropiedad.Body as MemberExpression; | |
var attr = bodyExpr.Member.GetCustomAttribute<ObsoleteAttribute>(); | |
return attr != null; | |
} | |
} |
Ejemplos de uso de las expresiones de árbol
Proveedores de consultas
Tanto Linq to SQL como Linq to Entities (Entity Framework) necesitan transformar expresiones descritas en código .Net a su expresión equivalente en SQL antes de enviar la consulta al servidor. Examinemos por ejemplo la siguiente expresión utilizada en Entity Framework sobre el métodoWhere
donde podemos especificar un predicado para obtener una colección filtrada de una entidad:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
List<Clientes> vips = contextoBd.Clientes | |
.Where(c => c.Volumen > 20000) | |
.ToList(); |
Where
acepta un árbol de expresión con un delegado de tipo Func
con un parámetro de entrada del tipo de la entidad sobre la que estamos invocando el Where
, en nuestro caso Clientes
, y con un parámetro de salida de tipo booleano. Entity Framework nos devolverá los clientes cuya función devuelva un valor true. ¿Cómo es capaz Entity Framework de generar la consulta SQL necesaria para cualquier función que nosotros le indiquemos?
La respuesta está en el árbol de expresión. Entity Framework no necesita compilar ni ejecutar la expresión que le hemos pasado. Lo que necesita es convertir los miembros descritos en la expresión en una cadena de consulta propia del lenguaje SQL. El caso anterior lo que hace es convertir el árbol de expresión en la cosulta:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
SELECT | |
[Extent1].[IdCliente] AS [IdCliente], | |
[Extent1].[Nombre] AS [Nombre], | |
[Extent1].[NIF] AS [NIF], | |
[Extent1].[VolumenVentas] AS [VolumenVentas] | |
FROM [dbo].[Clientes] AS [Extent1] | |
WHERE [Extent1].[Volumen] > cast(20000 as decimal(18)) |
Select
y realizar una proyección sobre un sólo campo, la consulta que utiliza Entity Framework se simplifica:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
List<string> vips2 = contextoBd.Clientes | |
.Where(c => c.Volumen > 20000) | |
.Select(c => c.Nombre) | |
.ToList(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
SELECT | |
[Extent1].[Nombre] AS [Nombre] | |
FROM [dbo].[Clientes] AS [Extent1] | |
WHERE [Extent1].[Volumen] > cast(20000 as decimal(18)) |
IEnumerable
, y Linq to SQL y Linq to Entities trabajan con colecciones que implementan la interfaz IQuereyable
.
A su vez, los métodos de extensión de
IEnumerable
esperan parámetros delegados y los métodos de extensión de IQuereyable
esperan parámetros de expresiones de árbol.Estas dos son las firmas del método
Where
de una colección IEnumerable
y de una colección IQuereyable
. Como se puede apreciar son equivalentes:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// IEnumerable | |
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate); | |
// IQuereyable | |
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate); |
IEnumerable
garantiza que una colección puede ser tratada como tal e iterada en un bucle foreach
, la interfaz IQuereyable
, además de implementar también la interfaz IEnumerable
, es capaz de realizar consultas por medio de un objeto Provider
proporcionado por la interfaz. El proveedor examinará el árbol de expresión y deberá "traducirla" a su consulta equivalente comprensible por el proveedor.
Métodos de extensión de HtmlHelper en ASP Net MVC
Observemos las siguientes líneas de código muy habituales en vistas razor para ASP Net MVC:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@model MyWebApi.Models.Cliente | |
@Html.DisplayNameFor(model => model.Nombre) | |
@Html.DisplayFor(model => model.Nombre) |
@Html
es un objeto de tipo HtmlHelper
que nos ayuda a generar código Html en una vista razor. Este objeto cuenta con una serie de métodos de extensión que aceptan parámetros de tipo árbol de expresión como por ejemplo: DisplayNameFor
, DisplayFor
, CheckBoxFor
, HiddenFor
, DisplayForModel
, etc. Estos métodos, además de ofrecernos un tipado fuerte sin opción a errores, proporcionan al objeto HtmlHelper
una información extra en el árbol de expresión.
Además de poder compilar e invocar el método para obtener el valor de la propiedad
Nombre
del modelo, a través del árbol de expresión, analizará tanto el parámetro de entrada (model
) como la propiedad seleccionada en el cuerpo de la expresión (model.Nombre
).
De esta manera será capaz de analizar por reflexión los atributos especificados en las propiedades del objeto Cliente.
Esto es muy útil para la representación en la vista de las etiquetas, sus valores e incluso para añadir al código Html todos los atributos necesarios de tipo data-* para la validación de campos de los formularios. ASP Net MVC, junto a la librería de jQuery Unobstrusive Validation ofrecen funcionalidad para la representación de los atributos necesarios para realizar la validación en el cliente.
Veamos un ejemplo con el siguiente modelo y vista de
Cliente
:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Cliente | |
{ | |
[Required] | |
public int IdCliente { get; set; } | |
[DisplayName("Nombre del Cliente")] | |
public string Nombre { get; set; } | |
[Required] | |
[StringLength(10, ErrorMessage ="El NIF no puede exceder de 10 caracteres")] | |
public string NIF { get; set; } | |
[Range(300, 3000, ErrorMessage ="Valor fuera de rango")] | |
[DisplayName("Volumen de ventas del año en curso")] | |
public decimal VolumenVentas { get; set; } | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Html.DisplayNameFor(model => model.IdCliente) | |
@Html.DisplayFor(model => model.IdCliente) | |
@Html.DisplayNameFor(model => model.Nombre) | |
@Html.DisplayFor(model => model.Nombre) | |
@Html.DisplayNameFor(model => model.NIF) | |
@Html.DisplayFor(model => model.NIF) | |
@Html.DisplayNameFor(model => model.VolumenVentas) | |
@Html.DisplayFor(model => model.VolumenVentas) |
DisplayNameFor
y DisplayFor
, que aceptan un árbol de expresión como parámetro, obtenemos una representación como la siguiente:Como se puede observar se ha utilizado el atributo
DisplayName
del modelo para la representación de las etiquetas de cada campo.Sin embargo, lo mejor viene para las vistas de edición donde tenemos un formulario con entrada de datos. Continuando con el modelo
Cliente
del mismo ejemplo, veamos la siguiente vista de edición:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Html.LabelFor(model => model.IdCliente) | |
@Html.EditorFor(model => model.IdCliente) | |
@Html.ValidationMessageFor(model => model.IdCliente, "", new { @class = "text-danger" }) | |
@Html.LabelFor(model => model.Nombre) | |
@Html.EditorFor(model => model.Nombre) | |
@Html.ValidationMessageFor(model => model.Nombre, "", new { @class = "text-danger" }) | |
@Html.LabelFor(model => model.NIF) | |
@Html.EditorFor(model => model.NIF) | |
@Html.ValidationMessageFor(model => model.NIF, "", new { @class = "text-danger" }) | |
@Html.LabelFor(model => model.VolumenVentas) | |
@Html.EditorFor(model => model.VolumenVentas) | |
@Html.ValidationMessageFor(model => model.VolumenVentas, "", new { @class = "text-danger" }) |
DisplayName
, Required
, Range
...) y los métodos de extensión LabelFor
, EditorFor
y ValidationMessageFor
, los cuales aceptan un árbol de expresión como parámetro, obtenemos el código Html necesario para la representación de las etiquetas, la validación en cliente y los textos de error producidos en la validación.Este sería el código generado de forma totalmente automática para el campo de texto de IdCliente:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<input class="text-box single-line" | |
data-val="true" | |
data-val-number="El campo IdCliente debe ser un número." | |
data-val-required="El campo IdCliente es obligatorio." | |
id="IdCliente" name="IdCliente" type="number" value="0"> | |
<span class="field-validation-valid text-danger" | |
data-valmsg-for="IdCliente" | |
data-valmsg-replace="true"> | |
</span> |
Si queremos personalizar la forma en que se representa el código Html recomiendo implementar nuestro propio método de extensión para que se pueda usar desde la vista. Podemos personalizar nuestros propios atributos y aplicar lógica adicional para moldear la representación final. Incluso podemos hacer que envuelva a uno de los métodos proporcionados por ASP Net MVC y decorarlo con nuestra lógica.
El siguiente ejemplo muestra un método de extensión del objeto
HtmlHelper
en el que comprobamos la existencia del atributo personalizado AlineamientoAttribute
en la propiedad de nuestro modelo y según el valor de su propiedad Alinamiento
añadimos una clase css distinta al código Html generado:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace System.Web.Mvc.Html | |
{ | |
public static class MisExtensiones | |
{ | |
public static MvcHtmlString EditorForAlineado<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression) | |
{ | |
var bodyExpr = expression.Body as MemberExpression; | |
var attri = bodyExpr.Member.GetCustomAttribute<AlineamientoAttribute>(); | |
switch (attri?.Alineamiento) | |
{ | |
case Alineamiento.Izda: | |
return html.TextBoxFor(expression, new { @class = "text-left" }); | |
case Alineamiento.Centro: | |
return html.TextBoxFor(expression, new { @class = "text-right" }); | |
case Alineamiento.Derecha: | |
return html.TextBoxFor(expression, new { @class = "text-center" }); | |
default: | |
return html.EditorFor(expression); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class AlineamientoAttribute : Attribute | |
{ | |
public Alineamiento Alineamiento { get; set; } | |
public AlineamientoAttribute(Alineamiento alineamiento) | |
{ | |
this.Alineamiento = alineamiento; | |
} | |
} | |
public enum Alineamiento | |
{ | |
Izda, | |
Centro, | |
Derecha | |
} |
Muy efectivo
ResponderEliminar