Setembro de 2016

Volume 31 – Número 9

ASP.NET Core – Fatias de Recurso do ASP.NET Core MVC

Por Steve Smith

Aplicativos Web maiores exigem uma organização melhor do que os menores. Com aplicativos de grande porte, a estrutura organizacional padrão usada pelo ASP.NET MVC (e Core MVC) começa a funcionar contra você. Você pode usar duas técnicas simples para atualizar sua abordagem organizacional e acompanhar um aplicativo em crescimento.

O padrão MVC (Modelo-Exibição-Controlador) é maduro, mesmo no espaço do Microsoft ASP.NET. A primeira versão do ASP.NET MVC saiu em 2009 e a primeira reinicialização completa da plataforma, ASP.NET Core MVC, saiu na metade deste ano. Durante esse período, à medida que o ASP.NET MVC evoluiu, a estrutura de projeto padrão permaneceu inalterada: pastas para Controladores e Exibições e frequentemente para Modelos (ou talvez ViewModels). Na verdade, se você criar um novo aplicativo ASP.NET Core hoje, verá que essas pastas serão criadas pelo modelo padrão, como mostra a Figura 1.

Estrutura Padrão do Modelo de Aplicativo Web ASP.NET Core
Figura 1 Estrutura Padrão do Modelo de Aplicativo Web ASP.NET Core

Há muitas vantagens nessa estrutura organizacional. Ela é conhecida. Se você já trabalhou em um projeto do ASP.NET MVC nos últimos anos, a reconhecerá imediatamente. Ela é organizada. Se você estiver procurando um controlador ou uma exibição, terá uma boa ideia de onde começar. Quando você está começando um novo projeto, essa estrutura organizacional funciona razoavelmente bem, pois ainda não há muitos arquivos. No entanto, à medida que o projeto cresce, cresce também a fricção envolvida na localização do arquivo controlador ou de exibição desejado dentro do número crescente de arquivos e pastas nessas hierarquias.

Para entender o que eu estou dizendo, imagine se você tiver organizado os arquivos de seu computador nessa mesma estrutura. Em vez de ter pastas separadas para projetos ou tipos de trabalho diferentes, você tivesse apenas diretórios organizados exclusivamente pelos tipos de arquivos. Haveria pastas para Documentos de Texto, PDFs, Imagens e Planilhas. Ao trabalhar em uma tarefa específica que envolve vários tipos de documentos, seria necessário manter a alternância entre as diferentes pastas e rolar e pesquisar nos diversos arquivos de cada pasta que não estivessem relacionados à tarefa em mãos. É exatamente assim que você trabalha nos recursos dentro de um aplicativo MVC organizado no modo padrão.

Isso é um problema pela tendência de falta de coesão dos grupos de arquivos organizados de acordo com o tipo, em vez de por finalidade. Coesão se refere ao nível de interligação dos elementos de um módulo. Em um projeto ASP.NET MVC comum, um determinado controlador fará referência a uma ou mais exibições relacionadas (em uma pasta que corresponde ao nome do controlador). O controlador e a exibição farão referência a um ou mais ViewModels relacionados à responsabilidade do controlador. Normalmente, no entanto, poucas exibições ou tipos de ViewModel são usados por mais de um tipo de controlador (e, normalmente, o modelo de domínio ou modelo de persistência é movido para seu próprio objeto separado).

Um Projeto de Exemplo

Considere um projeto simples, com a tarefa de gerenciar quatro conceitos de aplicativo pouco relacionados: Ninjas, Plantas, Piratas e Zumbis. O exemplo real permite apenas que você liste, exiba e adicione esses conceitos. No entanto, imagine que exista uma complexidade adicional que envolveria mais exibições. A estrutura organizacional padrão para esse projeto pareceria com a Figura 2.

Exemplo de Projeto com Organização Padrão
Figura 2 Exemplo de Projeto com Organização Padrão

Para trabalhar em uma nova funcionalidade que envolve Piratas, seria necessário navegar até Controladores e encontrar PiratesController. Em seguida, navegar em Exibições até Piratas até o arquivo de exibição apropriado. Mesmo com apenas cinco controladores, é possível ver que há muita navegação entre as pastas. Isso normalmente piora quando a raiz do projeto inclui muito mais pastas, pois Controladores e Exibições não estão próximos alfabeticamente (então outras pastas costumam ficar entre essas duas na lista de pastas).

Uma abordagem alternativa à organização de arquivos de acordo com o tipo é organizá-los seguindo a linha de atividade do aplicativo. Em vez de pastas para Controladores, Modelos e Exibições, seu projeto teria pastas organizadas por recursos ou áreas de responsabilidade. Ao trabalhar em um bug ou recurso relacionado a um recurso específico do aplicativo, seria necessário manter menos pastas abertas, pois os arquivos relacionados poderiam ser armazenados juntos. Isso pode ser feito de várias maneiras, incluindo o uso do recurso interno Áreas e a aplicação de sua própria convenção para pastas de recursos.

Como o ASP.NET Core MVC Vê os Arquivos

Vale a pena gastar um momento para falarmos sobre como o ASP.NET Core MVC trabalha com os tipos padrão de arquivos usados por um aplicativo criado nele. A maioria dos arquivos envolvidos no lado do servidor do aplicativo será de classes escritas em alguma linguagem .NET. Esses arquivos de código podem residir em qualquer lugar no disco, contanto que possam ser compilados e consultados pelo aplicativo. Particularmente, os arquivos de classe do Controlador não precisam ser armazenados em uma pasta específica. Diversos tipos de classes de modelo (modelo de domínio, modelo de exibição, modelo de persistência, etc.) são os mesmos e podem residir facilmente em projetos separados do projeto ASP.NET MVC Core. Você pode organizar e reorganizar a maioria dos arquivos de código no aplicativo da forma como quiser.

No entanto, as Exibições são diferentes. As Exibições são arquivos de conteúdo. O local onde eles são armazenados em relação às classes do controlador do aplicativo é irrelevante, mas é importante que o MVC saiba onde procurar por eles. As Áreas proporcionam suporte integrado para localização de exibições em locais diferentes da pasta Exibições padrão. Também é possível personalizar o modo como o MVC determina o local das exibições.

Como organizar Projetos MVC Usando Áreas

As Áreas proporcionam uma forma de organizar módulos independentes dentro de um aplicativo ASP.NET MVC. Cada Área tem uma estrutura de pastas que imita as convenções raiz do projeto. Portanto, seu aplicativo MVC teria as mesmas convenções de pasta raiz e uma pasta adicional chamada Áreas, dentro da qual existiria uma pasta para cada seção do aplicativo, contendo pastas para Controladores e Exibições (e talvez Modelos ou ViewModels, se isso for desejado).

As Áreas são um recurso incrível que permitem a segmentação de um aplicativo de grande porte em subaplicativos separados e logicamente distintos. Os Controladores, por exemplo, podem ter o mesmo nome nas áreas e, na verdade, é comum ter uma classe HomeController em cada área dentro de um aplicativo.

Para adicionar suporte às Áreas em um projeto do ASP.NET MVC Core, basta criar uma nova pasta de nível raiz chamada Áreas. Nessa pasta, crie uma nova pasta para cada parte de seu aplicativo que você queira organizar dentro de uma Área. Depois, dentro dessa pasta, adicione novas pastas para Controladores e Exibições.

Assim, os arquivos do controlador devem ficar em:

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

Seus controladores precisam ter um atributo Area aplicado a eles para avisar a estrutura que eles pertencem a uma área específica:

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

Suas exibições devem ficar em:

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

Quaisquer links para exibições que foram movidas para as áreas devem ser atualizados. Se você estiver usando auxiliares de marcação, especifique o nome da área como parte do auxiliar de marcação. Por exemplo:

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

Links entre exibições dentro da mesma área podem omitir o atributo asp-­area.

A última coisa que você precisa fazer para oferecer suporte às áreas em seu aplicativo é atualizar as regras de roteamento padrão para o aplicativo em Startup.cs no método Configurar:

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 exemplo, o exemplo de aplicativo para gerenciamento de vários Ninjas, Piratas, etc. poderia usar Áreas para conseguir a estrutura de organização do projeto, conforme mostra a Figura 3.

Como Organizar um Projeto ASP.NET Core com Áreas
Figura 3 Como Organizar um Projeto ASP.NET Core com Áreas

O recurso Áreas fornece um aprimoramento com relação à convenção padrão fornecendo pastas separadas para cada seção lógica do aplicativo. As Áreas são um recurso interno no ASP.NET Core MVC e exigem uma configuração mínima. Se você ainda não as estiver usando, considere-as uma maneira fácil de agrupar seções relacionadas de seu aplicativo e separá-las do restante do aplicativo.

No entanto, a organização Áreas ainda usa muitas pastas. É possível perceber isso no espaço vertical necessário para mostrar o número relativamente baixo de arquivos na pasta Áreas. Se você não tiver muitos controladores por área e não tiver muitas exibições por controlador, essa sobrecarga de pastas poderá acrescentar tanta fricção quanto na convenção padrão.

Felizmente, você pode facilmente criar sua própria convenção.

Pastas de Recursos no ASP.NET Core MVC

Além da convenção de pastas padrão ou do uso do recurso interno de Áreas, a maneira mais popular de organizar projetos de MVC é com pastas por recurso. Isso vale especialmente para equipes que adotaram a funcionalidade de entrega em fatias verticais (confira bit.ly/2abpJ7t), pois a maioria das preocupações de interface do usuário da fatia vertical pode existir dentro de uma ou mais dessas pastas de recursos.

Ao organizar seu projeto por recurso (em vez de tipo de arquivo), você normalmente terá uma pasta raiz (como Recursos) dentro da qual você terá uma subpasta por recurso. Isso é muito parecido com a forma de organização das áreas. No entanto, dentro de cada pasta de recursos, você incluirá todos os controladores, exibições e tipos de ViewModel exigidos. Na maioria dos aplicativos, isso resulta em uma pasta com talvez cinco a quinze itens, todos fortemente relacionados entre si. Todo o conteúdo da pasta de recursos pode ser mantido na exibição no Gerenciador de Soluções. Veja um exemplo dessa organização para o exemplo de projeto na Figura 4.

Organização da Pasta de Recursos
Figura 4 Organização da Pasta de Recursos

Perceba que até mesmo as pastas Controladores e Exibições no nível raiz foram eliminadas. Agora, a home page do aplicativo está em sua própria pasta de recursos chamada Home e os arquivos compartilhados, como _Layout.cshtml, estão em uma pasta Compartilhada dentro da pasta Recursos. Essa estrutura de organização do projeto pode ser facilmente dimensionada e permite que os desenvolvedores mantenham seu foco em uma quantidade muito menor de pastas enquanto trabalham em uma seção específica de um aplicativo.

Neste exemplo, ao contrário do que acontece em Áreas, não há a necessidade de rotas adicionais e nenhum atributo é necessário para os controladores (perceba, no entanto, que os nomes de controlador devem ser exclusivos entre os recursos nesta implementação). Para oferecer suporte a essa organização, você precisa de um IViewLocationExpander e IControllerModelConvention personalizados. Ambos são usados, junto com alguns ViewLocationFormats personalizados, para configurar o MVC em sua classe de Inicialização.

Para um determinado controlador, é útil saber com qual recurso ele está associado. As Áreas conseguem isso usando atributos. Essa abordagem usa uma convenção. A convenção espera que o controlador esteja em um namespace chamado “Recursos” e que o próximo item na hierarquia de namespaces após “Recursos” seja o nome do recurso. Esse nome é adicionado às propriedades disponíveis durante a localização da exibição, como mostra a 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;
  }
}

Adicione essa convenção como parte do MvcOptions ao adicionar MVC na Inicialização:

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

Para substituir a lógica de localização de exibição normal usada pelo MVC pela convenção baseada em recursos, limpe a lista de View­LocationFormats usada por MVC e substitua por sua própria lista. Isso é feito como parte da chamada AddMvc, como mostra a Figura 6.

Figura 6 Substituindo a Lógica de Localização de Exibição Normal Usada pelo MVC

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());
  }

Por padrão, essas cadeias de caracteres de formato incluem espaços reservados para ações (“{0}”), controladores (“{1}”) e áreas (“{2}”). Essa abordagem adiciona um quarto token (“{3}”) para recursos.

Os formatos de localização de exibição usados devem oferecer suporte às exibições com o mesmo nome, mas usados por controladores diferentes dentro de um recurso. Por exemplo, é bastante comum ter mais de um controlador em um recurso e, para vários controladores, ter um método de Índice. Isso recebe suporte por meio da pesquisa de exibições em uma pasta com o mesmo nome de controlador. Assim, NinjasController.Index e SwordsController.Index localizariam exibições em /Features/Ninjas/Ninjas/Index.cshtml e /Features/Ninjas/Swords/Index.cshtml, respectivamente (confira a Figura 7).

Múltiplos Controladores por Recurso
Figura 7 Múltiplos Controladores por Recurso

Perceba que isso é opcional. Se seus recursos não precisam remover a ambiguidade das exibições (por exemplo, pelo recurso ter apenas um controlador), basta colocar as exibições diretamente na pasta de recursos. Além disso, se você preferir usar prefixos de arquivo em vez de pastas, ajuste facilmente a cadeia de caracteres de formato para usar “{3}{1}” em vez de “{3}/{1}”, resultando em nomes de arquivo de exibição como NinjasIndex.cshtml e SwordsIndex.cshtml.

As exibições compartilhadas também recebem suporte, na raiz da pasta de recursos e em uma subpasta Compartilhada.

A interface de IViewLocationExpander expõe um método, ExpandViewLocations, que é usado pela estrutura para identificar pastas que contêm exibições. Essas pastas são pesquisadas quando uma ação retorna uma exibição. Essa abordagem exige apenas o ViewLocation­Expander para substituir o token “{3}” pelo nome de recurso do controlador especificado pelo FeatureConvention descrito anteriormente:

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 oferecer suporte à publicação correta, também será necessário atualizar o publishOptions do project.json para incluir a pasta Recursos:

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

A nova convenção de uso de uma pasta chamada Recursos está completamente sob seu controle, junto com o modo como as pastas são organizadas dentro dela. Ao modificar um conjunto de View­LocationFormats (e possivelmente o comportamento do tipo FeatureViewLocationExpander), você pode ter controle total sobre o local das exibições de seu aplicativo, que é a única coisa necessária para reorganizar seus arquivos, pois os tipos de controlador são descobertos independentemente da pasta onde estão localizados.

Pasta de Recursos Lado a Lado

Se você quiser experimentar a pasta de Recursos lado a lado com as convenções padrão de Área e Exibição de MVC, poderá fazer isso apenas com algumas pequenas modificações. Em vez de apagar o ViewLocationFormats, insira os formatos de recurso no início da lista (observe que a ordem está invertida):

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 oferecer suporte aos recursos combinados com áreas, modifique também a coleção 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");

E os Modelos?

Os leitores mais astutos perceberão que eu não movi meus tipos de modelo para a pasta de recursos (ou Áreas). Neste exemplo, eu não tenho tipos de ViewModel separados, pois os modelos que estou usando são incrivelmente simples. Em um aplicativo real, provavelmente seu domínio ou modelo de persistência terá mais complexidade do que suas exibições precisam e isso será definido em seu próprio projeto separado. Seu aplicativo MVC provavelmente definirá tipos de ViewModel que contêm apenas os dados necessários para uma determinada exibição, otimizados para exibição (ou consumo de uma solicitação de API do cliente). Esses tipos de ViewModel devem ser colocados na pasta de recursos onde serão usados (e deve ser algo raro compartilhar esses tipos entre recursos).

Conclusão

O exemplo inclui todas as três versões do aplicativo do organizador NinjaPiratePlant­Zombie com suporte à adição e exibição de cada tipo de dados. Baixe-o (ou exiba-o no GitHub) e pense em como cada abordagem funcionaria no contexto de um aplicativo no qual você está trabalhando hoje. Experimente adicionar uma Área ou uma pasta de recursos a um aplicativo maior no qual você trabalha e decida se você prefere trabalhar com fatias de recursos como a organização de nível superior da estrutura de pastas do seu aplicativo em vez de ter pastas de nível superior com base nos tipos de arquivo.

O código-fonte deste exemplo está disponível em bit.ly/29MxsI0.


Steve Smithé um treinador, mentor e consultor independente, bem como um ASP.NET MVP. Ele contribuiu com dezenas de artigos para a documentação oficial do ASP.NET Core (docs.asp.net) e ajuda as equipes a incorporar rapidamente o ASP.NET Core. Entre em contato com ele em ardalis.com.


Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Ryan Nowak
Ryan Nowak é um desenvolvedor que trabalha na equipe de ASP.NET da Microsoft.