Septiembre de 2016

Volumen 31, número 9

ASP.NET Core: Sectores de características para ASP.NET Core MVC

Por Steve Smith

Las aplicaciones web de envergadura requieren una mejor organización que las pequeñas. Con aplicaciones grandes, la estructura organizativa predeterminada que usa ASP.NET MVC (y Core MVC) empieza a funcionar en su contra. Puede usar dos sencillas técnicas para actualizar el enfoque organizativo y mantenerse al ritmo de una aplicación en crecimiento.

El patrón modelo-vista-controlador (MVC) es maduro, incluso en el espacio de Microsoft ASP.NET. La primera versión de ASP.NET MVC se publicó en 2009 y la primera nueva versión completa de la plataforma, ASP.NET Core MVC, se publicó a principios de verano. En este tiempo, como ASP.NET MVC ha evolucionado, la estructura de proyecto predeterminada ha permanecido sin cambios: las carpetas para los controladores (Controllers), las vistas (Views) y a menudo para modelos (Models, o quizás ViewModels). De hecho, si hoy crea una nueva aplicación de ASP.NET Core, verá estas carpetas creadas mediante la plantilla predeterminada, que se muestran en la Figura 1.

Estructura de plantilla de aplicación web de ASP.NET Core predeterminada
Figura 1. Estructura de plantilla de aplicación web de ASP.NET Core predeterminada

Esta estructura organizativa aporta numerosas ventajas. Resulta familiar; si ha trabajado en un proyecto de ASP.NET MVC en los últimos años, la reconocerá de inmediato. Es organizada; si está buscando un controlador o una vista, sabrá por dónde comenzar. Cuando está empezando un nuevo proyecto, esta estructura organizativa funciona razonablemente bien, porque aún no hay muchos archivos. Sin embargo, a medida que el proyecto crece, también crece la fricción a la hora de encontrar el archivo de vista o el controlador deseados dentro del cada vez mayor número de archivos y carpetas en estas jerarquías.

Para ver a lo que me refiero, imagine si organizara los archivos de su equipo con esta misma estructura. En lugar de disponer de carpetas separadas para distintos proyectos o tipos de trabajo, solo tendría directorios organizados únicamente mediante tipos de archivos. Podría haber carpetas para los documentos de textos, archivos PDF, imágenes y hojas de cálculo. Cuando trabajase en una tarea concreta en la que estuvieran involucrados varios tipos de documentos, tendría que estar cambiando entre las distintas carpetas y desplazarse o buscar por los numerosos archivos de cada carpeta que no están relacionados con la tarea en cuestión. Así es exactamente cómo se trabaja con características dentro de una aplicación MVC organizada de la forma predeterminada.

El motivo por el que esto es un problema es que los grupos de archivos organizados por tipo, en lugar de por propósito, suelen sufrir de falta de cohesión. Por cohesión se entiende el nivel al que los elementos de un módulo encajan entre sí. En un proyecto de ASP.NET MVC típico, un controlador determinado hará referencia a una o varias vistas relacionadas (en una carpeta que corresponda al nombre del controlador). Tanto el controlador como las vistas harán referencia a uno o varios elementos ViewModel relacionados con la responsabilidad del controlador. No obstante, normalmente pocos tipos de vistas o elementos ViewModel se usarán por parte de más de un tipo de controlador (y habitualmente el modelo de dominio o el modelo de persistencia se mueven a su propio modelo separado).

Proyecto de ejemplo

Considere un proyecto simple que tenga como tarea administrar cuatro conceptos de la aplicación poco relacionados: Ninjas, Plants, Pirates y Zombies. El ejemplo real solo permite enumerar, ver y agregar estos conceptos. Sin embargo, imagine que existiese una complejidad añadida que involucrara más vistas. La estructura organizativa predeterminada de este proyecto se parecería a la de la Figura 2.

Proyecto de ejemplo con organización predeterminada
Figura 2. Proyecto de ejemplo con organización predeterminada

Para trabajar en una nueva funcionalidad relacionada con Pirates, debería navegar hacia abajo hasta los controladores y encontrar PiratesController, y después navegar hacia abajo desde Views hasta Pirates dentro del archivo de vista adecuado. Incluso solo con cinco controladores, puede ver que implica una gran cantidad de navegación hacia arriba y abajo por las carpetas. A menudo, esto empeora cuando la raíz del proyecto incluye muchas más carpetas, ya que Controllers y Views no están cerca la una de la otra alfabéticamente (por lo que las carpetas adicionales suelen encontrarse entre ellas dos en la lista de carpetas).

Un enfoque alternativo de la organización de archivos por su tipo es organizarlos en función de lo que haga la aplicación. En lugar de tener carpetas para controladores, modelos y vistas, el proyecto tendría las carpetas organizadas en torno a características o áreas de responsabilidad. Cuando se trabaje en un error o una característica relacionados con una característica concreta de la aplicación, sería posible mantener menos carpetas abiertas, ya que los archivos relacionados se podrían almacenar juntos. Esto se puede conseguir de distintas maneras, incluyendo el uso de la característica integrada Areas e implementando su propia convención para las carpetas de características.

Cómo ve los archivos ASP.NET Core MVC

Merece la pena emplear un momento en hablar sobre la forma en que ASP.NET Core MVC trabaja con los tipos de archivos estándares que una aplicación creada en él usa. La mayoría de archivos involucrados en el lado servidor de la aplicación serán clases escritas en algún lenguaje .NET. Estos archivos de código pueden residir en cualquier parte del disco, siempre que la aplicación pueda hacer referencias a ellos y puedan compilarse. En concreto, no es necesario que los archivos de la clase Controller se almacenen en ninguna carpeta concreta. Varios tipos de clases de modelo (modelo de dominio, modelo de vista, modelo de persistencia, etc.) son lo mismo y pueden residir sin problema en proyectos separados del proyecto de ASP.NET MVC Core. Puede organizar y reorganizar la mayoría de los archivos de código de la aplicación de la forma que prefiera.

Las vistas, sin embargo, son distintas. Las vistas son archivos de contenido. El lugar en que se almacenan en relación con las clases del controlador de la aplicación no tiene relevancia, pero es importante que MVC conozca dónde debe buscarlos. Las áreas ofrecen compatibilidad integrada para encontrar vistas en ubicaciones distintas de la carpeta Views predeterminada. También puede personalizar cómo determina MVC la ubicación de las vistas.

Organización de proyectos MVC mediante áreas

Las áreas ofrecen una forma de organizar módulos independientes dentro de una aplicación de ASP.NET MVC. Cada área tiene una estructura de carpetas que imita las convenciones de la raíz del proyecto. Por lo tanto, la aplicación MVC tendría las mismas convenciones de la carpeta raíz y una carpeta adicional denominada Areas, dentro de la cual habría una carpeta para cada sección de la aplicación, que contendría carpetas para controladores y vistas (y tal vez modelos o elementos ViewModels, si se desease).

Las áreas son una potente característica que permiten segmentar una aplicación grande en subaplicaciones separadas y lógicamente distintas. Los controladores, por ejemplo, pueden tener el mismo nombre en distintas áreas y, de hecho, es habitual tener una clase HomeController en cada área dentro de una aplicación.

Para agregar compatibilidad con Areas en un proyecto de ASP.NET MVC Core, solo sería necesario crear una nueva carpeta a nivel de raíz denominada Areas. En esta carpeta, cree una nueva carpeta por cada parte de la aplicación que quiera organizar dentro de un área. Después, dentro de esta carpeta, agregue las nuevas carpetas Controllers y Views.

Sus archivos de controlador deberían por tanto ubicarse en:

/Areas/[area name]/Controllers/[controller name].cs

Los controladores deben tener un atributo Area aplicado para que el marco sepa que pertenecen a un área determinada:

namespace WithAreas.Areas.Ninjas.Controllers
{
  [Area("Ninjas")]
  public class HomeController : Controller

Por tanto, las vistas deberían ubicarse en:

/Areas/[area name]/Views/[controller name]/[action name].cshtml

Los vínculos que tuviera a las vistas que se hayan movido a áreas deben actualizarse. Si usa aplicaciones auxiliares de etiquetas, puede especificar el nombre del área como parte de la aplicación auxiliar de etiquetas. Por ejemplo:

<a asp-area="Ninjas" asp-controller="Home" asp-action="Index">Ninjas</a>

Los vínculos entre vistas dentro de la misma área pueden omitir el atributo asp-­area.

Lo último que debe hacer para admitir áreas en la aplicación es actualizar las reglas de enrutamiento predeterminadas de la aplicación en el archivo Startup.cs del método Configure:

app.UseMvc(routes =>
{
  // Areas support
  routes.MapRoute(
    name: "areaRoute",
    template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
  routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");
});

Por ejemplo, la aplicación de ejemplo para administrar varios elementos Ninja, Pirate, etc. podría utilizar Areas para conseguir la estructura de organización del proyecto, como se muestra en la Figura 3.

Organización de un proyecto de ASP.NET Core con Areas
Figura 3. Organización de un proyecto de ASP.NET Core con Areas

La característica Areas ofrece una mejora respecto a la convención predeterminada al proporcionar carpetas separadas para cada sección lógica de la aplicación. Las áreas conforman una característica integrada de ASP.NET Core MVC y requieren una configuración mínima. Si aún no las usa, téngalas presentes como una forma sencilla de agrupar secciones relacionadas de la aplicación y mantenerlas separadas del resto de la aplicación.

Sin embargo, la organización de Areas sigue haciendo un uso intensivo de carpetas. Puede ver esto en el espacio vertical requerido para mostrar el número de archivos relativamente pequeño de la carpeta Areas. Si no tiene muchos controladores por área y no dispone de muchas vistas por controlador, esta sobrecarga de carpetas puede agregar fricción de forma muy similar a la convención predeterminada.

Afortunadamente, puede crear su propia convención con facilidad.

Carpetas de características en ASP.NET Core MVC

Aparte de la convención de carpetas predeterminada y el uso de la característica integrada Areas, la forma más conocida de organizar proyectos MVC es con carpetas por características. Esto es especialmente cierto para los equipos que hayan adoptado la entrega de funcionalidad en sectores verticales (consulte bit.ly/2abpJ7t), porque la mayoría de aspectos de la interfaz de usuario de un sector vertical pueden existir dentro de una de estas carpetas de características.

Cuando se organiza un proyecto por características (en lugar de por tipos de archivo), normalmente tendrá una carpeta raíz (como Features) dentro de la cual habrá una subcarpeta por característica. Esto es muy parecido a la forma en que se organizan las áreas. Sin embargo, dentro de cada carpeta de características, se incluirán todos los tipos ViewModel, los controladores y las vistas requeridos. En la mayoría de aplicaciones, el resultado es una carpeta con posiblemente de cinco a quince elementos en ella, todos ellos estrechamente relacionados los unos con los otros. Todo el contenido de la carpeta de características se puede mantener a la vista en el Explorador de soluciones. Puede ver un ejemplo de esta organización con el proyecto de ejemplo en la Figura 4.

Organización de la carpeta Feature
Figura 4. Organización de la carpeta Feature

Observe que se han eliminado hasta las carpetas Controllers y Views del nivel de la raíz. La página principal de la aplicación ahora se encuentra en su propia carpeta de características denominada Home, y los archivos compartidos como _Layout.cshtml también se encuentran en una carpeta Shared dentro de la carpeta Features. Esta estructura de organización de proyectos se escala correctamente y permite que los desarrolladores centren su atención en muchas menos carpetas mientras trabajan en una sección concreta de una aplicación.

En este ejemplo, al contrario de lo que sucede con Areas, no se requieren rutas adicionales y no se necesitan atributos para los controladores (no obstante, tenga en cuenta que los nombres de los controladores deben ser únicos entre características de esta implementación). Para admitir esta organización, necesita unos elementos IViewLocationExpander e IControllerModelConvention personalizados. Los dos se usan, junto con algún elemento ViewLocationFormats personalizado, para configurar MVC en la clase Startup.

Para un controlador determinado, resulta útil conocer con qué característica se asociará. Las áreas logran esto mediante el uso de atributos; este enfoque usa una convención. La convención espera que el controlador esté en un espacio de nombres denominado "Features", y que el elemento siguiente de la jerarquía del espacio de nombres después de "Features" sea el nombre de la característica. Este nombre se agrega a las propiedades que están disponibles durante la localización de la vista, como se muestra en la Figura 5.

Figura 5. FeatureConvention : IControllerModelConvention

{
  public void Apply(ControllerModel controller)
  {
    controller.Properties.Add("feature", 
      GetFeatureName(controller.ControllerType));
  }
  private string GetFeatureName(TypeInfo controllerType)
  {
    string[] tokens = controllerType.FullName.Split('.');
    if (!tokens.Any(t => t == "Features")) return "";
    string featureName = tokens
      .SkipWhile(t => !t.Equals("features",
        StringComparison.CurrentCultureIgnoreCase))
      .Skip(1)
      .Take(1)
      .FirstOrDefault();
    return featureName;
  }
}

Puede agregar esta convención como parte de MvcOptions cuando agregue MVC en Startup:

services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));

Para reemplazar la lógica de localización de vistas normal que MVC utiliza por la convención basada en características, puede borrar la lista de elementos View­LocationFormat que MVC usa y sustituirla por su propia lista. Esto se realiza como parte de la llamada a AddMvc, como se muestra en la Figura 6.

Figura 6. Sustitución de la lógica de localización de vistas normal que MVC usa

services.AddMvc(o => o.Conventions.Add(new FeatureConvention()))
  .AddRazorOptions(options =>
  {
    // {0} - Action Name
    // {1} - Controller Name
    // {2} - Area Name
    // {3} - Feature Name
    // Replace normal view location entirely
    options.ViewLocationFormats.Clear();
    options.ViewLocationFormats.Add("/Features/{3}/{1}/{0}.cshtml");
    options.ViewLocationFormats.Add("/Features/{3}/{0}.cshtml");
    options.ViewLocationFormats.Add("/Features/Shared/{0}.cshtml");
    options.ViewLocationExpanders.Add(new FeatureViewLocationExpander());
  }

De manera predeterminada, estas cadenas de formato incluyen marcadores de posiciones para acciones ("{0}"), controladores ("{1}") y áreas ("{2}"). Este enfoque agrega un cuarto token ("{3}") para características.

Los formatos de localización de vistas utilizados deberían admitir las vistas con el mismo nombre, pero que controladores distintos dentro de una característica utilizan. Por ejemplo, es bastante habitual disponer de más de un controlador en una característica y que varios controladores tengan un método Index. Esto se admite mediante la búsqueda de vistas en una carpeta que coincide con el nombre del controlador. Por tanto, NinjasController.Index y SwordsController.Index buscarían las vistas en /Features/Ninjas/Ninjas/Index.cshtml y /Features/Ninjas/Swords/Index.cshtml, respectivamente (consulte la Figura 7).

Varios controladores por característica
Figura 7. Varios controladores por característica

Tenga en cuenta que esto es opcional, si las características no tienen que eliminar ambigüedades de las vistas (por ejemplo, porque la característica solo tenga un controlador), puede simplemente poner las vistas directamente dentro de la carpeta de características. Además, si prefiere usar prefijos de archivo en lugar de carpetas, podría ajustar fácilmente la cadena de formato para usar "{3}{1}" en lugar de "{3}/{1}", lo que daría como resultado nombres de archivos de vistas como NinjasIndex.cshtml y SwordsIndex.cshtml.

También se admiten las vistas compartidas, tanto en la raíz de la carpeta de características como en una subcarpeta Shared.

La interfaz IViewLocationExpander expone un método, ExpandViewLocations, que el marco usa para identificar las carpetas que contienen vistas. Estas carpetas se buscan cuando una acción devuelve una vista. Este enfoque solo requiere que ViewLocation­Expander reemplace el token "{3}" por el nombre de la característica del controlador, que el elemento FeatureConvention descrito anteriormente especifica:

public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
  IEnumerable<string> viewLocations)
{
  // Error checking removed for brevity
  var controllerActionDescriptor =
    context.ActionContext.ActionDescriptor as ControllerActionDescriptor;
  string featureName = controllerActionDescriptor.Properties["feature"] as string;
  foreach (var location in viewLocations)
  {
    yield return location.Replace("{3}", featureName);
  }
}

Para admitir la publicación correctamente, también deberá actualizar el elemento publishOptions de project.json para que incluya la carpeta Features:

"publishOptions": {
  "include": [
    "wwwroot",
    "Views",
    "Areas/**/*.cshtml",
    "Features/**/*.cshtml",
    "appsettings.json",
    "web.config"
  ]
},

La nueva convención consistente en usar una carpeta denominada Features está completamente bajo su control, junto con la forma en que las carpetas se organizan dentro de ella. Si modifica el conjunto de elementos View­LocationFormat (y posiblemente el comportamiento del tipo FeatureViewLocationExpander), puede tener un control total sobre el lugar en que se ubican las vistas de la aplicación, que es lo único necesario para reorganizar los archivos, ya que los tipos de controladores se detectan independientemente de la carpeta en la que se encuentren.

Carpetas de características en paralelo

Si quiere probar las carpetas Feature en paralelo con las convenciones de View y Area de MVC predeterminadas, puede hacerlo con solo unas pequeñas modificaciones. En lugar de borrar ViewLocationFormats, inserte los formatos de características dentro de la lista (tenga en cuenta que el orden es inverso):

options.ViewLocationFormats.Insert(0, "/Features/Shared/{0}.cshtml");
options.ViewLocationFormats.Insert(0, "/Features/{3}/{0}.cshtml");
options.ViewLocationFormats.Insert(0, "/Features/{3}/{1}/{0}.cshtml");

Para admitir las características combinadas con áreas, modifique también la colección de AreaViewLocationFormats:

options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/Shared/{0}.cshtml");
options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/{3}/{0}.cshtml");
options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/{3}/{1}/{0}.cshtml");

¿Qué ocurre con los modelos?

Los lectores más astutos se habrán dado cuenta de que no he movido mis tipos de modelos a las carpetas de características (ni a Areas). En este ejemplo, no dispongo de tipos ViewModel separados, ya que los modelos que uso son tremendamente simples. En una aplicación del mundo real, probablemente los modelos de persistencia o de dominio tendrán más complejidad de la que requieren las vistas, y se definirá en su propio proyecto separado. La aplicación MVC probablemente definirá tipos ViewModel que contengan solo los datos necesarios para una vista determinada, optimizada para su visualización (o su consumo desde la solicitud de la API de un cliente). Estos tipos ViewModel deberían ubicarse en la carpeta de características en la que se usen (y debería ser excepcional que estos tipos se compartieran entre características).

Resumen

En el ejemplo se incluyen las tres versiones de la aplicación organizadora de NinjaPiratePlant­Zombie, con compatibilidad para agregar y ver cada tipo de datos. Descárguelo (o visualícelo en GitHub) y piense cómo funcionaría cada enfoque en el contexto de una aplicación en la que esté trabajando actualmente. Pruebe a agregar un área o una carpeta de características en una aplicación más grande en la que esté trabajando y decida si prefiere trabajar con sectores de características como organización de nivel superior de la estructura de carpetas de la aplicación, en lugar de disponer de carpetas de nivel superior basadas en tipos de archivos.

El código fuente de este ejemplo está disponible en bit.ly/29MxsI0.


Steve Smithes instructor, mentor y asesor independiente, además de MVP de ASP.NET. Ha aportado decenas de artículos a la documentación oficial de ASP.NET Core (docs.asp.net) y ayuda a equipos para que saquen el máximo provecho de ASP.NET Core rápidamente. Puede ponerse en contacto con él en ardalis.com.


Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Ryan Nowak
Ryan Nowak es un desarrollador que trabaja en el equipo de ASP.NET de Microsoft.