Muy buenas!
Imagina que tienes la siguiente entidad de un modelo de datos de Entity Framework:
1: public class Product
2: {
3: public int Id {get; set;}
4: [Required]
5: public string Name { get; set; }
6: public string ShortDescription { get; set; }
7: public string LongDescription { get; set; }
8: public string Remmarks { get; set; }
9: public string LegalNotices { get; set; }
10: public string TermsOfUse { get; set; }
11: [Column(TypeName="Image")]
12: public byte[] Icon { get; set; }
13: [Column(TypeName = "Image")]
14: public byte[] Screenshot { get; set; }
15: [Column(TypeName = "Image")]
16: public byte[] ScreenshotHD { get; set; }
17: public decimal Price { get; set; }
18: }
Y que tienes un controlador que recoge los productos que tengas en la BBDD y los pasa a una vista:
1: public ActionResult Index()
2: {
3: IEnumerable<Product> products = null;
4: using (var ctx = new MyContext())
5: {
6: products = ctx.Products.ToList();
7: }
8:
9: return View(products);
10: }
Donde la vista se limita a mostrar usar tan solo un par de propiedades de la clase Product:
1: <h2>Produtcs list</h2>
2:
3: <ul>
4: @foreach (var item in Model)
5: {
6: <li>@item.Name - @Html.ActionLink("View", "Details", new {id=@item.Id})</li>
7: }
8: </ul>
Probablemente habrás visto código muy parecido en muchos blogs o ejemplos. Dicho código funciona correctamente, pero… ¿cual es el problema?
Pues bien, en el caso de entidades grandes, como puede ser esta Product (fíjate que hay 3 campos de imágen, y un par de campos de texto que podrían ser muy largos), el problema es que la consulta que está generando Entity Framework es:
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[ShortDescription] AS [ShortDescription], [Extent1].[LongDescription] AS [LongDescription], [Extent1].[Remmarks] AS [Remmarks], [Extent1].[LegalNotices] AS [LegalNotices], [Extent1].[TermsOfUse] AS [TermsOfUse], [Extent1].[Icon] AS [Icon], [Extent1].[Screenshot] AS [Screenshot], [Extent1].[ScreenshotHD] AS [ScreenshotHD], [Extent1].[Price] AS [Price]FROM [dbo].[Products] AS [Extent1]
Es decir, estamos trasladando todos los campos desde la base de datos hacia el servidor web para tan solo usar un par de campos en la vissta (Name e Id).
Esto, en el caso de que seleccionemos muchas entidades grandes puede ser un problema. Por supuesto, la solución es muy sencilla, decirle a Entity Framework que tan solo seleccione los campos que deseemos:
1: products = ctx.Products.Select(p => new { p.Id, p.Name}).ToList();
Con esto la query generada contiene tan solo los campos deseados:
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name]FROM [dbo].[Products] AS [Extent1]
El tema está ahora en como pasamos esta información a la vista. Anteriormente la vista tenia la declaración @model IEnumerable<Product> pero ahora esto ya no nos sirve y ASP.NET MVC se quejará (con razón) de que el tipo del modelo de la vista no es el que le pasamos, y nos dará un error.
Quitar la directiva @model de la vista no funciona y recibirás un error parecido al siguiente ‘object’ does not contain a definition for ‘Name’. Si quitamos la directiva @model, el modelo de la vista pasa a ser de tipo Object lo que no nos sirve porque ni Name ni Id están definidas en Object.
Una solución es generar un ViewModel, es decir una clase que contenga los datos que la vista requiere:
1: public class ProductListViewModel
2: {
3: public int Id { get; set; }
4: public string Name { get; set; }
5: }
Luego en EF debemos crear una colección de este objeto, en lugar del objeto anónimo:
1: var products = ctx.Products.Select(p => new ProductListViewModel
2: {
3: Id = p.Id,
4: Name = p.Name
5: }).ToList();
Ahora colocando la directiva @model IEnumerable<ProductListViewModel> en nuestra vista todo funciona correctamente.
El problema ahora viene si tenemos muchas vistas que listen distintos campos Product (p. ej. una con Name e Id, otra con Name, Id, Icon y ShortDescription, otra con Name, LongDescription y Remmarks, etc…). Si sigues esta técnica te vas a encontrar con muchos view models distintos, lo cual puede ser tedioso de mantener.
ASP.NET MVC tiene el concepto de tipo de modelo dinámico. Para ello en la vista puedes poner @model dynamic.
Pero… no es oro todo lo que reluce. Si cambias la directiva a @model dynamic y vuelves al código donde usábamos un objeto anónimo… Obtendrás de nuevo el error ‘object’ does not contain a definition for ‘Name’. Es decir, a pesar de que declaras el tipo de modelo como dinámico en la vista, parece que para ASP.NET MVC el modelo sigue siendo un object.
La razón es que cuando usas @model dynamic el objeto que pasas debe estar preparado para interaccionar con el DLR y los objetos anónimos no lo están. La solución pasa por usar ExpandoObject que si que lo está. Entonces el código del controlador pasa a:
1: var products = ctx.Products.Select(p => new
2: {
3: p.Id,
4: p.Name
5: }).ToList();
6:
7: List<dynamic> model = new List<dynamic>();
8: foreach (var item in products)
9: {
10: dynamic data = new ExpandoObject();
11: data.Id = item.Id;
12: data.Name = item.Name;
13: model.Add(data);
14: }
15:
16: return View(model);
Ahora usando @model dynamic todo funciona correctamente. Por supuesto parece que el remedio es peor que la enfermedad: ¡El código es bastante más pesado y tienes que realizar esta copia de “propiedades” del objeto anónimo al ExpandoObject!
Ahí es donde una característica de ExpandoObject y un método de extensión te pueden ayudar. La característica es que ExpandoObject implementa IDictionary<string,object> y agregar una propiedad a ExpandoObject es equivalente a agregar una entrada en el diccionario. Así podemos tener un par de métodos de extensión sencillos:
1: static class ExpandoObjectExtensions
2: {
3: public static dynamic CopyFrom(this ExpandoObject source, object data)
4: {
5: var dict = source as IDictionary<string, object>;
6: foreach (var property in data.GetType().GetProperties())
7: {
8: dict.Add(property.Name, property.GetValue(data, null));
9: }
10:
11: return source;
12: }
13:
14: public static IEnumerable<dynamic> Dynamize<T>(this IEnumerable<T> source)
15: {
16: foreach (var entry in source)
17: {
18: var expando = new ExpandoObject();
19: yield return expando.CopyFrom(entry);
20: }
21: }
22: }
Y ahora nuestro código en el controlador queda simplemente como:
1: var products = ctx.Products.Select(p => new
2: {
3: p.Id,
4: p.Name
5: }).ToList().Dynamize();
¡Y listos! Usando @model dynamic nuestra vista sigue funcionando y nos olvidamos de ir creando viewmodels y de copiar propiedades arriba y abajo.
Espero que os haya sido interesante! 😉
¡Un saludo!