ViewModels en MVC / MVVM / Separación de capas: ¿mejores prácticas?

Soy bastante nuevo en el uso de ViewModels y me pregunto, ¿es aceptable que ViewModel contenga instancias de modelos de dominio como propiedades, o las propiedades de esos modelos de dominio deberían ser propiedades del mismo ViewModel? Por ejemplo, si tengo una clase Album.cs

 public class Album { public int AlbumId { get; set; } public string Title { get; set; } public string Price { get; set; } public virtual Genre Genre { get; set; } public virtual Artist Artist { get; set; } } 

Por lo general, usted haría que ViewModel tuviera una instancia de la clase Album.cs , o tendría el ViewModel con propiedades para cada una de las propiedades de la clase Album.cs .

 public class AlbumViewModel { public Album Album { get; set; } public IEnumerable Genres { get; set; } public IEnumerable Artists { get; set; } public int Rating { get; set; } // other properties specific to the View } public class AlbumViewModel { public int AlbumId { get; set; } public string Title { get; set; } public string Price { get; set; } public IEnumerable Genres { get; set; } public IEnumerable Artists { get; set; } public int Rating { get; set; } // other properties specific to the View } 

    La parte divertida: esto no está limitado a viewmodels en MVC , en realidad es una cuestión de separación de las “buenas capas antiguas de datos / negocios / ui”, es decir, separación de preocupaciones. Lo ilustraré más adelante, pero por ahora; tenga en cuenta que se aplica a MVVM o cualquier otro patrón de diseño también.

    ¿Es aceptable que ViewModel contenga instancias de modelos de dominio?

    Básicamente no, aunque veo que sucede mucho. Depende un poco del nivel de quick-win de su proyecto.

    Déjame dar un ejemplo. Imagine el siguiente modelo de vista:

     public class FooViewModel { public string Name {get; set;} public DomainClass Genre {get;set;} } 

    y la siguiente DomainClass

     //also applies to database data/POCO classes public class DomainClass { public int Id {get; set;} public string Name {get;set;} } 

    Por lo tanto, en algún lugar de tu controlador rellenas FooViewModel y lo pasas a tu vista.

    Ahora, considere los siguientes escenarios:

    1) El modelo de dominio cambia.

    En este caso, probablemente también deba ajustar la vista, esto es una mala práctica en el contexto de la separación de las preocupaciones.

    Si ha separado ViewModel del DomainModel, sería suficiente un pequeño ajuste en las asignaciones (ViewModel => DomainModel (y viceversa)).

    2) DomainClass tiene propiedades anidadas y su vista solo muestra el nombre de GenreName .

    He visto esto ir mal en escenarios reales.

    En este caso, un problema común es que el uso de @Html.EdittorFor dará lugar a entradas para el objeto nested. Esto podría incluir Id y otra información sensible. Después de este curso, te encontrarás creando entradas hidden . Si combina esto con un enlazador de modelos del lado del servidor o un automapper, es realmente difícil bloquear la manipulación de Id ocultos con herramientas como Firebug.

    Aunque es posible, quizás fácil, bloquear algunos de esos campos, cuantos más objetos de dominio / datos nesteds tenga, más difícil será asegurar esta parte. Y tenga en cuenta que es posible que desee cambiar su DomainModel por una razón que no necesariamente se dirige a la vista. Por lo tanto, con cada cambio en su DomainModel debe tener en cuenta que podría afectar la vista y los aspectos de seguridad del controlador.

    3) En asp.net-MVC es común usar atributos de validación.

    ¿Realmente quieres que tu dominio contenga metadatos sobre tus vistas? ¿O aplicar view-logic a su capa de datos? ¿Su validación de vista es siempre la misma que la validación de dominio? ¿Tiene la misma lógica de validación? ¿Está utilizando su aplicación cruzada de modelos de dominio? etc.

    Creo que está claro que esta no es la ruta a seguir.

    4 más

    Puedo darte más escenarios, pero es más una cuestión de gusto para lo que es más atractivo. Solo espero que en este punto saques el punto 🙂 Sin embargo, prometí una ilustración:

    Scematic

    Ahora, para quick-wins realmente sucios y quick-wins funcionará, pero no creo que debas quererlo.

    Es solo un poco más esfuerzo construir un modelo de vista, que generalmente es para un 80% o más similar al modelo de dominio. Esto podría parecer hacer asignaciones innecesarias, pero cuando surja la primera diferencia conceptual, descubrirá que valió la pena el esfuerzo 🙂

    Entonces, como alternativa, propongo la siguiente configuración para un caso general:

    • crear un modelo de vista
    • crear un modelo de dominio
    • crear un modelo de datos
    • use una biblioteca como automapper para crear mapeos de uno a otro (esto ayudará a mapear Foo.FooProp a OtherFoo.FooProp )

    Los beneficios son, por ejemplo; si crea un campo adicional en una de las tablas de su base de datos, no afectará su vista. Puede afectar a su capa de negocios o mapeos, pero allí se detendrá. Por supuesto, la mayor parte del tiempo también desea cambiar su vista, pero en este caso no es necesario . Por lo tanto, mantiene el problema aislado en una parte de su código.

    web api / data-layer

    Otro ejemplo más concreto de cómo esto funcionará en un escenario Web-API / EF:

    Web Api Datalayer EF

    Nota

    Como @mrjoltcola declaró: también hay un componente de sobreingeniería a tener en cuenta. Si no se aplica lo mencionado anteriormente y se puede confiar en los usuarios / progtwigdores, es bueno que se vaya. Pero tenga en cuenta que el mantenimiento y la reutilización disminuirán debido a la mezcla DomainModel / ViewModel.

    Las opiniones varían, a partir de una combinación de mejores prácticas técnicas y preferencias personales.

    No hay nada de malo con el uso de objetos de dominio en sus modelos de vista, o incluso el uso de objetos de dominio como su modelo, y muchas personas lo hacen. Algunos creen firmemente en la creación de modelos de vista para cada vista individual, pero personalmente, creo que muchas aplicaciones están sobrediseñadas por desarrolladores que aprenden y repiten un enfoque con el que se sienten cómodos. La verdad es que hay varias formas de lograr el objective utilizando versiones más nuevas de ASP.NET MVC.

    El mayor riesgo, cuando utiliza una clase de dominio común para su modelo de vista y su capa de negocios y persistencia, es la inyección de modelo. Agregar nuevas propiedades a una clase de modelo puede exponer esas propiedades fuera del límite del servidor. Un atacante puede ver propiedades que no debería ver (serialización) y alterar valores que no debe alterar (carpetas de modelo).

    Para protegerse contra la inyección, use prácticas seguras que sean relevantes para su enfoque general. Si planea usar objetos de dominio, asegúrese de usar listas blancas o negras (inclusión / exclusión) en el controlador o mediante anotaciones de modelo de encuadernación. Las listas negras son más convenientes, pero los desarrolladores perezosos que escriben futuras revisiones pueden olvidarse de ellas o no ser conscientes de ellas. Las listas blancas ([Enlazar (Incluir = …)] son ​​obligatorias, requieren atención cuando se agregan nuevos campos, por lo que actúan como un modelo de vista en línea.

    Ejemplo:

     [Bind(Exclude="CompanyId,TenantId")] public class CustomerModel { public int Id { get; set; } public int CompanyId { get; set; } // user cannot inject public int TenantId { get; set; } // .. public string Name { get; set; } public string Phone { get; set; } // ... } 

    o

     public ActionResult Edit([Bind(Include = "Id,Name,Phone")] CustomerModel customer) { // ... } 

    La primera muestra es una buena manera de hacer cumplir la seguridad multiusuario en toda la aplicación. La segunda muestra permite personalizar cada acción.

    Sea coherente en su enfoque y documente claramente el enfoque utilizado en su proyecto para otros desarrolladores.

    Le recomiendo que siempre utilice los modelos de vista para las características relacionadas con el inicio de sesión / perfil para forzarse a “ordenar” los campos entre el consiller web y la capa de acceso a datos como un ejercicio de seguridad.