Desenvolver aplicativos ASP.NET Core MVC

Dica

Esse conteúdo é um trecho do livro eletrônico, para Projetar os Aplicativos Web Modernos com o ASP.NET Core e o Azure, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.

Miniatura da capa do livro eletrônico Arquitetar aplicativos Web modernos com o ASP.NET Core e o Azure.

"Não é importante acertar na primeira vez. É muito importante acertar na última vez". – Andrew Hunt e David Thomas

O ASP.NET Core é uma estrutura multiplataforma de software livre para a criação de aplicativos Web modernos otimizados para a nuvem. Os aplicativos ASP.NET Core são leves e modulares, com suporte interno para a injeção de dependência, o que aumenta a capacidade de teste e a facilidade de manutenção. Combinado com o MVC, que é compatível com a criação de APIs Web modernas, além de aplicativos baseados em exibição, o ASP.NET Core é uma estrutura avançada para a criação de aplicativos Web empresariais.

MVC e Razor Pages

O ASP.NET Core MVC oferece diversos recursos úteis para a criação de APIs e aplicativos baseados na Web. O termo MVC significa "Model-View-Controller", um padrão de interface do usuário que divide a responsabilidade de responder às solicitações do usuário em várias partes. Além de seguir esse padrão, você também pode implementar recursos em seus aplicativos ASP.NET Core, como as Razor Pages.

As Razor Pages são integradas ao ASP.NET Core MVC e usam os mesmos recursos para roteamento, model binding, filtros, autorização etc. No entanto, em vez de ter pastas e arquivos separados para controladores, modelos, exibições etc. e usar o roteamento baseado em atributo, as Razor Pages são colocadas em uma só pasta ("/Pages"), fazem o roteamento com base no local relativo nessa pasta e processam solicitações com manipuladores em vez de ações do controlador. Como resultado, no trabalho com Razor Pages, todos os arquivos e as classes necessários normalmente ficam no mesmo local, não são distribuídos em todo o projeto Web.

Saiba mais sobre como o MVC, as Razor Pages e os padrões relacionados são aplicados no aplicativo de exemplo eShopOnWeb.

Ao criar um aplicativo ASP.NET Core, você deve ter um plano em mente para o tipo de aplicativo que deseja. Ao criar um projeto no IDE ou usando o comando dotnet new da CLI, você escolherá entre vários modelos. Os modelos de projeto mais comuns são Vazio, API Web, Aplicativo Web e Aplicativo Web (Model-View-Controller). Embora você só possa decidir isso durante a criação de um projeto, essa não é uma decisão irrevogável. O projeto de API Web usa controladores Model-View-Controller padrão. Ele apenas não tem Exibições por padrão. Da mesma forma, o modelo de Aplicativo Web padrão usa Razor Pages e, portanto, também não tem uma pasta Exibições. Você poderá adicionar uma pasta de Exibições a esses projetos mais tarde para permitir o comportamento com base na exibição. Os projetos de API Web e Model-View-Controller não incluem uma pasta Pages por padrão, mas você poderá adicioná-la mais tarde para permitir o comportamento com base em Razor Pages. Considere esses três modelos como suportes a três tipos diferentes de interação do usuário padrão: dados (API Web), baseado em página e baseado em exibição. No entanto, você pode combinar todos esses modelos em um só projeto, se desejar.

Por que usar Razor Pages?

As Razor Pages são a abordagem padrão para novos aplicativos Web no Visual Studio. Elas oferecem uma maneira mais simples de criar recursos de aplicativo baseados em página, como formulários que não são de SPA. Com controladores e exibições, era comum que os aplicativos tivessem controladores muito grandes que funcionavam com muitas dependências e modelos de exibição diferentes e retornavam várias exibições. Isso resultava em mais complexidade e, geralmente, em controladores que não seguiram o princípio de responsabilidade única ou os princípios abertos/fechados com eficiência. As Razor Pages resolvem esse problema encapsulando a lógica do lado do servidor para uma determinada "página" lógica em um aplicativo Web com sua marcação Razor. Uma Razor Page que não tem nenhuma lógica do lado do servidor só pode consistir em um arquivo Razor (por exemplo, "Index.cshtml"). No entanto, a maioria das Razor Pages menos triviais têm uma classe de modelo de página associada, que, por convenção, tem o mesmo nome que o arquivo do Razor, com uma extensão ".cs" (por exemplo, "Index.cshtml.cs").

O modelo de página de uma Razor Page combina as responsabilidades de um controlador MVC e de um viewmodel. Em vez de manipular as solicitações com métodos de ação do controlador, são executados manipuladores de modelo de página, como "OnGet()", renderizando suas próprias páginas associadas por padrão. As Razor Pages simplificam o processo de criação de páginas individuais em um aplicativo ASP.NET Core, fornecendo ainda todos os recursos de arquiteturas ASP.NET Core MVC. Elas são uma boa opção padrão para a nova funcionalidade baseada em página.

Quando usar o MVC

Se você estiver criando APIs Web, o padrão MVC fará mais sentido do que tentar usar Razor Pages. Se o projeto for expor apenas os pontos de extremidade de API Web, comece usando o modelo de projeto API Web. Caso contrário, será fácil adicionar controladores e pontos de extremidade de API associados a qualquer aplicativo ASP.NET Core. Use a abordagem MVC baseada em exibição se você estiver migrando um aplicativo ASP.NET MVC 5 existente ou anterior para o ASP.NET Core MVC e quiser fazer isso com o mínimo de esforço. Depois de fazer a migração inicial, você poderá avaliar se faz sentido adotar as Razor Pages para os novos recursos ou até mesmo como uma migração em grande escala. Para obter mais informações sobre a migração de aplicativos .NET 4.x para o .NET 8, confira o livro eletrônico Como migrar aplicativos ASP.NET para o ASP.NET Core.

Se você optar por criar o aplicativo Web usando Razor Pages ou exibições do MVC, o aplicativo terá um desempenho semelhante e incluirá suporte para injeção de dependência, filtros, model binding, validação e assim por diante.

Mapeando solicitações para respostas

Em sua essência, os aplicativos ASP.NET Core mapeiam as solicitações de entrada para as respostas de saída. Em um nível baixo, esse mapeamento é feito com middleware, e aplicativos e microsserviços ASP.NET Core simples podem ser compostos apenas por um middleware personalizado. Ao usar o ASP.NET Core MVC, você pode trabalhar em um nível um pouco superior, pensando em termos de rotas, controladores e ações. Cada solicitação de entrada é comparada com a tabela de roteamento do aplicativo e se uma rota correspondente é encontrada, o método de ação associado (pertencente a um controlador) é chamado para manipular a solicitação. Se nenhuma rota correspondente é encontrada, um manipulador de erro (nesse caso, retornando um resultado NotFound) é chamado.

Os aplicativos ASP.NET Core MVC podem usar rotas convencionais, rotas de atributo ou ambas. As rotas convencionais são definidas no código, especificando as convenções de roteamento com uma sintaxe parecida com o exemplo abaixo:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});

Nesse exemplo, uma rota chamada "default" foi adicionada à tabela de roteamento. Ele define um modelo de rota com espaço reservados para controller, action e id. Os espaço reservados controller e action têm o padrão especificado (Home e Index, respectivamente) e o espaço reservado id é opcional (em virtude de um "?" aplicado a ele). A convenção definida aqui indica que a primeira parte de uma solicitação deve corresponder ao nome do controlador, a segunda parte à ação, e, depois, se necessário, uma terceira parte representará um parâmetro de ID. As rotas convencionais normalmente são definidas em um só lugar para o aplicativo, como em Program.cs, em que o pipeline de middleware de solicitação está configurado.

As rotas de atributo são aplicadas aos controladores e às ações diretamente, em vez de serem especificadas globalmente. Essa abordagem tem a vantagem de facilitar muito mais a descoberta delas quando você está analisando um método específico, mas significa que as informações de roteamento não são mantidas em um só lugar no aplicativo. Com as rotas de atributo, você pode especificar com facilidade várias rotas para determinada ação, além de combinar rotas entre controladores e ações. Por exemplo:

[Route("Home")]
public class HomeController : Controller
{
    [Route("")] // Combines to define the route template "Home"
    [Route("Index")] // Combines to define route template "Home/Index"
    [Route("/")] // Does not combine, defines the route template ""
    public IActionResult Index() {}
}

As rotas podem ser especificadas no [HttpGet] e em atributos semelhantes, evitando a necessidade de adicionar atributos [Route] separados. As rotas de atributo também podem usar tokens para reduzir a necessidade de repetir os nomes do controlador ou da ação, conforme mostrado abaixo:

[Route("[controller]")]
public class ProductsController : Controller
{
    [Route("")] // Matches 'Products'
    [Route("Index")] // Matches 'Products/Index'
    public IActionResult Index() {}
}

As Razor Pages não usam o roteamento de atributo. Você pode especificar informações de modelo de rota adicionais para uma Razor Page como parte de sua diretiva @page:

@page "{id:int}"

No exemplo anterior, a página em questão corresponderia a uma rota com um parâmetro id inteiro. Por exemplo, a página Products.cshtml localizada na raiz de /Pages responderia a solicitações como esta:

/Products/123

Depois que for feita a correspondência de uma solicitação específica a uma rota, mas antes da chamada do método de ação, o ASP.NET Core MVC executará o model binding e a validação de modelos na solicitação. O model binding é responsável por converter os dados HTTP de entrada nos tipos .NET especificados como parâmetros do método de ação a ser chamado. Por exemplo, se o método de ação esperar um parâmetro int id, o model binding tentará fornecer esse parâmetro por meio de um valor fornecido como parte da solicitação. Para fazer isso, o model binding procurará valores em um formulário publicado, valores na própria rota e valores de cadeia de caracteres de consulta. Supondo que um valor id seja encontrado, ele será convertido em um inteiro antes de ser passado para o método de ação.

Após a associação do modelo, mas antes da chamada do método de ação, ocorre a validação de modelos. A validação de modelos usa atributos opcionais no tipo de modelo e pode ajudar a garantir que o objeto de modelo fornecido está em conformidade com determinados requisitos de dados. Determinados valores podem ser especificados conforme o necessário ou limitados a um determinado comprimento, intervalo numérico ou outros aspectos. Se os atributos de validação forem especificados, mas o modelo não estiver em conformidade com os respectivos requisitos, a propriedade ModelState.IsValid será false e o conjunto de regras de validação com falha estará disponível para envio ao cliente que está fazendo a solicitação.

Se você estiver usando a validação de modelos, sempre verifique se o modelo é válido antes de executar comandos de alteração do estado, para garantir que o aplicativo não seja corrompido por dados inválidos. Você pode usar um filtro para evitar a necessidade de adicionar código para essa validação em cada ação. Os filtros do ASP.NET Core MVC oferecem uma maneira de interceptar grupos de solicitações, de modo que as políticas comuns e os interesses paralelos possam ser aplicados de forma direcionada. Os filtros podem ser aplicados a ações individuais, a controladores inteiros ou globalmente a um aplicativo.

Para APIs Web, o ASP.NET Core MVC é compatível com a negociação de conteúdo, permitindo que as solicitações especifiquem como as respostas devem ser formatadas. Com base nos cabeçalhos fornecidos na solicitação, as ações que retornam dados formatarão a resposta em XML, JSON ou outro formato compatível. Esse recurso permite que a mesma API seja usada por vários clientes com diferentes requisitos de formato de dados.

Os projetos de API Web devem considerar o uso do atributo [ApiController], que pode ser aplicado aos controladores individuais, a uma classe base de controlador ou ao assembly inteiro. Esse atributo adiciona a verificação de validação automática de modelos, portanto, qualquer ação com um modelo inválido retornará um BadRequest com os detalhes dos erros de validação. O atributo também requer que todas as ações tenham uma rota de atributos, em vez de uma rota convencional, e retorna informações de ProblemDetails mais detalhadas em resposta aos erros.

Manter os controladores sob controle

Para aplicativos baseados em página, as Razor Pages fazem um ótimo trabalho de impedir que os controladores fiquem muito grandes. Cada página individual recebe os próprios arquivos e classes dedicadas apenas para seus manipuladores. Antes da introdução das Razor Pages, muitos aplicativos centrados em exibição tinham classes de controlador grandes responsáveis por muitas ações e exibições diferentes. Essas classes naturalmente aumentavam para conter várias responsabilidades e dependências, o que dificultava a manutenção. Se você achar que os controladores baseados em exibição estão crescendo muito, considere reformulá-los para usar Razor Pages ou introduzir um padrão, como um mediador.

O padrão de design de mediador é usado para reduzir o acoplamento entre classes, permitindo a comunicação entre elas. Em aplicativos ASP.NET Core MVC, esse padrão costuma ser utilizado para separar controladores em partes menores usando manipuladores para fazer o trabalho de métodos de ação. O pacote NuGet MediatR popular geralmente é usado para fazer isso. Normalmente, os controladores incluem vários métodos de ação diferentes e cada um deles pode exigir determinadas dependências. O conjunto de todas as dependências exigidas por uma ação precisa ser passado para o construtor do controlador. Quando o MediatR é usado, a única dependência do controlador é uma instância do mediador. Depois, cada ação usa a instância do mediador para enviar uma mensagem, que é processada por um manipulador. O manipulador é específico de uma só ação e, portanto, só precisa das dependências exigidas por essa ação. Um exemplo de um controlador que usa o mediador é mostrado aqui:

public class OrderController : Controller
{
    private readonly IMediator _mediator;

    public OrderController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet]
    public async Task<IActionResult> MyOrders()
    {
        var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name));
        return View(viewModel);
    }
    // other actions implemented similarly
}

Na ação MyOrders, a chamada para Send de uma mensagem GetMyOrders é tratada por essa classe:

public class GetMyOrdersHandler : IRequestHandler<GetMyOrders, IEnumerable<OrderViewModel>>
{
    private readonly IOrderRepository _orderRepository;
    public GetMyOrdersHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

  public async Task<IEnumerable<OrderViewModel>> Handle(GetMyOrders request, CancellationToken cancellationToken)
    {
        var specification = new CustomerOrdersWithItemsSpecification(request.UserName);
        var orders = await _orderRepository.ListAsync(specification);
        return orders.Select(o => new OrderViewModel
            {
                OrderDate = o.OrderDate,
                OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel()
                  {
                    PictureUrl = oi.ItemOrdered.PictureUri,
                    ProductId = oi.ItemOrdered.CatalogItemId,
                    ProductName = oi.ItemOrdered.ProductName,
                    UnitPrice = oi.UnitPrice,
                    Units = oi.Units
                  }).ToList(),
                OrderNumber = o.Id,
                ShippingAddress = o.ShipToAddress,
                Total = o.Total()
        });
    }
}

O resultado final dessa abordagem é que os controladores são muito menores e se concentram principalmente no roteamento e no model binding, enquanto os manipuladores individuais são responsáveis pelas tarefas específicas necessárias para um determinado ponto de extremidade. Essa abordagem também pode ser obtida sem mediador usando o pacote NuGet ApiEndpoints, que tenta oferece aos controladores de API os mesmos benefícios que as Razor Pages oferecem para os controladores baseados em exibição.

Referências – Mapeando solicitações para respostas

Trabalhando com dependências

O ASP.NET Core tem suporte interno para uma técnica conhecida como injeção de dependência, além de fazer uso dela internamente. A injeção de dependência é uma técnica que permite um acoplamento flexível entre diferentes partes de um aplicativo. Um acoplamento mais flexível é desejável porque facilita o isolamento de partes do aplicativo, permitindo o teste ou a substituição. Ele também torna menos provável que uma alteração em uma parte do aplicativo tenha um impacto inesperado em outro lugar do aplicativo. A injeção de dependência baseia-se no princípio da inversão de dependência e costuma ser fundamental para alcançar o princípio do aberto/fechado. Ao avaliar como o aplicativo funciona com suas dependências, tenha cuidado com o code smell adesão estática e lembre-se do aforismo "new é associação".

A adesão estática ocorre quando as classes fazem chamadas a métodos estáticos ou acessam propriedades estáticas que têm efeitos colaterais ou dependências na infraestrutura. Por exemplo, se você tiver um método que chama um método estático, que, por sua vez, grava em um banco de dados, o método terá um acoplamento rígido com o banco de dados. Qualquer coisa que interrompa essa chamada de banco de dados interromperá o método. O teste desses métodos é notoriamente difícil, pois testes desse tipo exigem bibliotecas fictícias comerciais para simular as chamadas estáticas ou podem ser testados somente com um banco de dados de teste em vigor. As chamadas estáticas que não têm nenhuma dependência de infraestrutura, principalmente as que são completamente sem estado, são ideais e não afetam o acoplamento nem a capacidade de teste (além de acoplar o código à chamada estática em si).

Muitos desenvolvedores entendem os riscos da adesão estática e do estado global, mas ainda acoplarão rigidamente o código com implementações específicas por meio da criação de instância direta. A frase "new é associação" deve ser um lembrete desse acoplamento e não uma reprovação geral do uso da palavra-chave new. Assim como ocorre com chamadas de método estático, novas instâncias de tipos que não têm nenhuma dependência externa normalmente não acoplam rigidamente o código com detalhes de implementação nem dificultam mais o teste. No entanto, sempre que for criada uma instância para uma classe, reserve um breve momento para considerar se faz sentido embutir essa instância específica em código em um local específico ou se é um melhor design solicitar essa instância como uma dependência.

Declarar as dependências

O ASP.NET Core foi criado com a ideia de que os métodos e as classes devem declarar suas dependências, solicitando-as como argumentos. Os aplicativos ASP.NET normalmente são configurados em Program. cs ou em uma classe Startup.

Observação

A configuração completa de aplicativos em Program.cs é a abordagem padrão para aplicativos .NET 6 (e posterior) e Visual Studio 2022. Os modelos de projeto foram atualizados para que você possa começar a usar essa nova abordagem. Os projetos ASP.NET Core ainda podem usar uma classe Startup, se desejado.

Configurar serviços em Program.cs

Para aplicativos muito simples, você pode transmitir as dependências diretamente no arquivo Program.cs usando um WebApplicationBuilder. Depois que todos os serviços necessários forem sido adicionados, o construtor será usado para criar o aplicativo.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

Configurar serviços em Startup.cs

O Startup.cs é configurado para dar suporte à injeção de dependência em vários pontos. Se você estiver usando uma classe Startup, forneça um construtor para que ela possa solicitar dependências por meio dele, desta forma:

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
    }
}

A classe Startup é interessante porque não tem nenhum requisito de tipo explícito. Ela não é herdada de uma classe base Startup especial, nem implementa nenhuma interface específica. Você pode atribuir um construtor a ela, ou não, e pode especificar quantos parâmetros no construtor desejar. Quando o host da Web que você configurou para o aplicativo for iniciado, ele chamará a classe Startup (se você o tiver instruído a usar uma) e usará a injeção de dependência para popular as dependências que a classe Startup requer. É claro que, se você solicitar parâmetros que não estiverem configurados no contêiner de serviços usado pelo ASP.NET Core, receberá uma exceção, mas desde que você continue usando as dependências reconhecidas pelo contêiner, poderá solicitar o que desejar.

A injeção de dependência baseia-se nos aplicativos ASP.NET Core desde o início, quando a instância de Startup é criada. Ela não para na classe Startup. Você também pode solicitar dependências no método Configure:

public void Configure(IApplicationBuilder app,
    IHostingEnvironment env,
    ILoggerFactory loggerFactory)
{

}

O método ConfigureServices é a exceção a esse comportamento. Ele precisa usar apenas um parâmetro do tipo IServiceCollection. Realmente não é preciso dar suporte à injeção de dependência, pois é, por um lado, responsável por adicionar objetos ao contêiner de serviços e, por outro, tem acesso a todos os serviços atualmente configurados por meio do parâmetro IServiceCollection. Assim, você pode trabalhar com dependências definidas na coleção de serviços do ASP.NET Core em cada parte da classe Startup, seja solicitando o serviço necessário como um parâmetro ou trabalhando com o IServiceCollection no ConfigureServices.

Observação

Se você precisar garantir que determinados serviços estejam disponíveis para a classe Startup, configure-os usando um IWebHostBuilder e o método ConfigureServices dentro da chamada CreateDefaultBuilder.

A classe Startup é um modelo de como você deve estruturar outras partes do aplicativo ASP.NET Core, de Controladores, Middleware, Filtros a seus próprios Serviços. Em cada caso, você deve seguir o Princípio das Dependências Explícitas, solicitando as dependências em vez de criá-las diretamente e aproveitando a injeção de dependência em todo o aplicativo. Tenha cuidado com o local em que você cria instâncias de implementações diretamente e como você as cria, especialmente serviços e objetos que trabalham com a infraestrutura ou que têm efeitos colaterais. Prefira trabalhar com as abstrações definidas no núcleo do aplicativo e passadas como argumentos para referências embutidas em código a tipos de implementação específicos.

Estruturando o aplicativo

Normalmente, os aplicativos monolíticos têm um único ponto de entrada. No caso de um aplicativo Web ASP.NET Core, o ponto de entrada será o projeto Web ASP.NET Core. No entanto, isso não significa que a solução precise consistir em apenas um único projeto. É útil dividir o aplicativo em camadas diferentes para seguir a separação de interesses. Depois de dividido em camadas, é útil ir além de pastas para projetos separados, que podem ajudar a obter o melhor encapsulamento. A melhor abordagem para atingir essas metas com um aplicativo ASP.NET Core é uma variação da Arquitetura Limpa abordada no capítulo 5. Seguindo essa abordagem, a solução do aplicativo incluirá bibliotecas separadas para a interface do usuário, a infraestrutura e o ApplicationCore.

Além desses projetos, projetos de teste separados são incluídos também (o teste é abordado no capítulo 9).

O modelo de objeto do aplicativo e as interfaces devem ser colocados no projeto de ApplicationCore. Esse projeto terá o menor número de dependências possível (e nenhuma em relação a questões de infraestrutura específicas) e os outros projetos da solução farão referência a ele. As entidades de negócios que precisam ser persistidas são definidas no projeto de ApplicationCore, assim como os serviços que não dependem diretamente da infraestrutura.

Os detalhes de implementação, como a persistência é executada ou como as notificações podem ser enviadas a um usuário, são mantidos no projeto de Infraestrutura. Esse projeto referenciará pacotes específicos à implementação, como o Entity Framework Core, mas não deve expor os detalhes sobre essas implementações fora do projeto. Os repositórios e serviços de infraestrutura devem implementar interfaces que são definidas no projeto de ApplicationCore e suas implementações de persistência são responsáveis por recuperar e armazenar entidades definidas no ApplicationCore.

O projeto de interface do usuário do ASP.NET Core é responsável pelas preocupações no nível da interface do usuário, mas não deve incluir detalhes da lógica de negócios ou infraestrutura. Na verdade, o ideal é que ele não tenha nem mesmo uma dependência no projeto de Infraestrutura, o que ajudará a garantir que nenhuma dependência entre os dois projetos seja introduzida acidentalmente. Isso pode ser feito usando um contêiner de DI de terceiros, como o Autofac, que permite que você defina regras de DI em classes de Módulo em cada projeto.

Outra abordagem para desacoplar o aplicativo dos detalhes de implementação é fazer com que o aplicativo chame microsserviços, talvez implantados em contêineres individuais do Docker. Isso fornece uma separação de interesses e um desacoplamento ainda maiores do que a utilização da DI entre dois projetos, mas traz uma complexidade adicional.

Organização do recurso

Por padrão, os aplicativos ASP.NET Core organizam sua estrutura de pastas para incluir Controladores e Exibições e, frequentemente, ViewModels. Em geral, o código do lado do cliente para dar suporte a essas estruturas do lado do servidor é armazenado separadamente na pasta wwwroot. No entanto, os aplicativos grandes podem enfrentar problemas com essa organização, pois o trabalho em um recurso específico geralmente exige o salto entre essas pastas. Isso fica cada vez mais difícil à medida que aumenta o número de arquivos e subpastas em cada pasta, resultando em uma grande quantidade de rolagem pelo Gerenciador de Soluções. Uma solução para esse problema é organizar o código do aplicativo por recurso, em vez de por tipo de arquivo. Esse estilo organizacional é normalmente conhecido como pastas de recurso ou fatias de recurso (confira também: Fatias verticais).

O ASP.NET Core MVC é compatível com Áreas para essa finalidade. Usando áreas, você pode criar conjuntos separados de pastas de Controladores e Exibições (bem como os modelos associados) em cada pasta de Área. A Figura 7-1 mostra uma estrutura de pastas de exemplo, usando Áreas.

Exemplo de organização de área

Figura 7-1. Organização da área de exemplo

Ao usar Áreas, você precisa usar atributos para decorar os controladores com o nome da área à qual eles pertencem:

[Area("Catalog")]
public class HomeController
{}

Você também precisa adicionar o suporte para área às rotas:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "areaRoute", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});

Além do suporte interno para Áreas, você também pode usar sua própria estrutura de pastas e convenções no lugar de atributos e rotas personalizadas. Isso permite que você tenha pastas de recurso que não incluíam pastas separadas para Exibições, Controladores, etc., mantendo a hierarquia mais simples e facilitando a visualização de todos os arquivos relacionados em um único local para cada recurso. Nas APIs, as pastas podem ser usadas para substituir controladores, e cada pasta pode conter todos os pontos de extremidade de API e os DTOs associados.

O ASP.NET Core usa tipos de convenção internos para controlar seu comportamento. Você pode modificar ou substituir essas convenções. Por exemplo, você pode criar uma convenção que receberá automaticamente o nome de recurso para determinado controlador com base em seu namespace (que geralmente se correlaciona com a pasta na qual o controlador está localizado):

public class 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;
    }
}

Depois, você especifica essa convenção como uma opção quando adiciona suporte para MVC ao aplicativo em ConfigureServices (ou em Program.cs):

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

// Program.cs
builder.Services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));

O ASP.NET Core MVC também usa uma convenção para localizar exibições. Você pode substituí-la por uma convenção personalizada, de modo que as exibições estejam localizadas nas pastas de recurso (usando o nome de recurso fornecido pela FeatureConvention, acima). Saiba mais sobre essa abordagem e baixe um exemplo funcional no artigo da MSDN Magazine, Fatias de recursos do ASP.NET Core MVC.

APIs e aplicativos Blazor

Se o aplicativo incluir um conjunto de APIs Web, que precisa ser protegido, essas APIs deverão ser configuradas como um projeto separado do aplicativo de exibição ou de Razor Pages. A separação das APIs, principalmente das APIs públicas, do aplicativo Web do lado do servidor tem vários benefícios. Esses aplicativos geralmente terão características exclusivas de implantação e de carga. Também é muito provável que eles adotem mecanismos diferentes de segurança, com aplicativos baseados em formulários padrão que aproveitam a autenticação baseada em cookie e APIs com maior probabilidade de usar a autenticação baseada em token.

Além disso, os aplicativos Blazor, seja usando o servidor Blazor ou o BlazorWebAssembly, devem ser criados como projetos separados. Os aplicativos têm características de tempo de execução e modelos de segurança diferentes. É provável que eles compartilhem tipos comuns com o aplicativo Web do lado do servidor (ou o projeto de API), e esses tipos devem ser definidos em um projeto compartilhado comum.

A adição de uma interface de administração BlazorWebAssembly ao eShopOnWeb exigiu a adição de vários novos projetos. O projeto BlazorWebAssembly em si, BlazorAdmin. Um novo conjunto de pontos de extremidade de API pública, usado pelo BlazorAdmin e configurado para usar a autenticação baseada em token, é definido no projeto PublicApi. E determinados tipos compartilhados usados pelos dois projetos são mantidos em um novo projeto BlazorShared.

Alguém pode perguntar, por que adicionar um projeto BlazorShared separado quando já existe um projeto ApplicationCore comum que poderia ser usado para compartilhar qualquer tipo exigido por PublicApi e BlazorAdmin? A resposta é que esse projeto inclui toda a lógica de negócios do aplicativo e, portanto, é muito maior do que o necessário e deve ser mantido em segurança no servidor. Lembre-se de que qualquer biblioteca referenciada por BlazorAdmin, será baixada nos navegadores dos usuários quando eles carregarem o aplicativo Blazor.

Dependendo da opção de usar ou não o padrão BFF Backends-For-Frontends), as APIs consumidas pelo aplicativo BlazorWebAssembly talvez não compartilhem totalmente seus tipos com Blazor. Especificamente, uma API pública que deve ser consumida por vários clientes diferentes pode definir seus próprios tipos de solicitação e de resultado, em vez de compartilhá-los em um projeto compartilhado específico do cliente. No exemplo do eShopOnWeb, considera-se que o projeto PublicApi esteja realmente hospedando uma API pública, portanto, nem todos os tipos de solicitação e resposta vêm do projeto BlazorShared.

Interesses paralelos

Conforme os aplicativos crescem, fica cada vez mais importante excluir interesses paralelos para eliminar a duplicação e manter a consistência. Alguns exemplos de interesses paralelos em aplicativos ASP.NET Core são autenticação, regras de validação de modelos, cache de saída e tratamento de erro, embora haja muitos outros. Os filtros do ASP.NET Core MVC permitem executar o código antes ou depois de determinadas etapas do pipeline de processamento de solicitações. Por exemplo, um filtro pode ser executado antes e após o model binding, antes e após uma ação ou antes e após o resultado de uma ação. Você também pode usar um filtro de autorização para controlar o acesso ao restante do pipeline. A Figura 7-2 mostra como solicitar fluxos de execução por meio de filtros, caso eles estejam configurados.

A solicitação é processada por meio de Filtros de autorização, Filtros de recurso, Model binding, Filtros de ação, Execução de ação e Conversão do resultado da ação, Filtros de exceção, Filtros de resultado e Execução de resultado. No caminho de saída, a solicitação é apenas processada pelos Filtros de resultado e os Filtros de recurso para se tornar uma resposta a ser enviada ao cliente.

Figura 7-2. Solicite a execução por meio de filtros e do pipeline de solicitação.

Normalmente, os filtros são implementados como atributos, de modo que você possa aplicá-los aos controladores ou às ações (ou até mesmo globalmente). Quando adicionados dessa maneira, os filtros especificados no nível da ação substituem ou se baseiam nos filtros especificados no nível do controlador, que, por sua vez, substituem os filtros globais. Por exemplo, o atributo [Route] pode ser usado para criar rotas entre controladores e ações. Da mesma forma, a autorização pode ser configurada no nível do controlador e, em seguida, substituída por ações individuais, como mostra o seguinte exemplo:

[Authorize]
public class AccountController : Controller
{
    [AllowAnonymous] // overrides the Authorize attribute
    public async Task<IActionResult> Login() {}
    public async Task<IActionResult> ForgotPassword() {}
}

O primeiro método, Login, usa o filtro [AllowAnonymous] (atributo) para substituir o filtro Autorizar definido no nível do controlador. A ação ForgotPassword (e qualquer outra ação na classe que não tem um atributo AllowAnonymous) exigirá uma solicitação autenticada.

Os filtros podem ser usados para eliminar a duplicação na forma de tratamento de erro comum de políticas para APIs. Por exemplo, uma política de API normal deve retornar uma resposta NotFound para solicitações que fazem referência às chaves que não existem e uma resposta BadRequest se houver falha na validação de modelos. O seguinte exemplo demonstra essas duas políticas em ação:

[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
    if ((await _authorRepository.ListAsync()).All(a => a.Id != id))
    {
        return NotFound(id);
    }
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    author.Id = id;
    await _authorRepository.UpdateAsync(author);
    return Ok();
}

Não permita que os métodos de ação fiquem desorganizados com um código condicional como este. Em vez disso, coloque as políticas em filtros que podem ser aplicados conforme necessário. Neste exemplo, a verificação de validação do modelo, que deve ocorrer sempre que um comando é enviado à API, pode ser substituído pelo seguinte atributo:

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
}

É possível adicionar o ValidateModelAttribute ao projeto como uma dependência do NuGet incluindo o pacote Ardalis.ValidateModel. Para as APIs, você pode usar o atributo ApiController para impor esse comportamento sem precisar usar um filtro ValidateModel separado.

Da mesma forma, um filtro pode ser usado para verificar se um registro existe e retornar 404 antes que a ação seja executada, eliminando a necessidade de executar essas verificações na ação. Depois de retirar as convenções comuns e organizar sua solução para separar a lógica de negócios e o código de infraestrutura da interface do usuário, os métodos de ação do MVC devem ser extremamente dinâmicos:

[HttpPut("{id}")]
[ValidateAuthorExists]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
    await _authorRepository.UpdateAsync(author);
    return Ok();
}

Leia mais sobre como implementar filtros e baixe um exemplo funcional no artigo da MSDN Magazine, Filtros do ASP.NET Core MVC do mundo real.

Se você tiver várias respostas comuns de APIs baseadas em cenários comuns, como erros de validação (solicitação inválida), recurso não encontrado e erros de servidor, considere o uso de uma abstração de resultado. A abstração de resultado retornaria pelos serviços usados pelos pontos de extremidade de API, e a ação do controlador ou o ponto de extremidade usaria um filtro para convertê-los em IActionResults.

Referências – estruturando aplicativos

Segurança

A proteção de aplicativos Web é um tópico extenso, com muitas considerações. Em seu nível mais básico, a segurança envolve a garantia de que você sabe de quem determinada solicitação é proveniente e, em seguida, a garantia de que essa solicitação tem acesso somente aos recursos que deveria. Autenticação é o processo de comparar as credenciais fornecidas com uma solicitação com aquelas em um armazenamento de dados confiável, para verificar se a solicitação deve ser tratada como proveniente de uma entidade conhecida. Autorização é o processo de restringir o acesso a determinados recursos com base na identidade do usuário. Uma terceira preocupação de segurança é proteger as solicitações contra interceptação por terceiros, para o qual você deve, pelo menos, garantir que o SSL é usado pelo aplicativo.

Identidade

O ASP.NET Core Identity é um sistema de associação que pode ser usado para dar suporte à funcionalidade de logon para o aplicativo. Ele tem suporte para contas de usuário local, bem como suporte para provedores de logon externo de provedores como a conta da Microsoft, Twitter, Facebook, Google e muito mais. Além do ASP.NET Core Identity, o aplicativo pode usar a autenticação do Windows ou um provedor de identidade de terceiros, como o Identity Server.

O ASP.NET Core Identity é incluído em novos modelos de projeto se a opção Contas de Usuário Individuais é marcada. Esse modelo inclui suporte para registro, logon, logons externos, senhas esquecidas e funcionalidade adicional.

Selecionar contas de usuário individuais em que o Identity será pré-configurado

Figura 7-3. Selecione contas de usuário individuais para que o Identity seja pré-configurado.

O suporte ao Identity é configurado em Program.cs ou em Startup e inclui a configuração de serviços, bem como de middleware.

Configurar o Identity em Program.cs

Em Program.cs, você configura serviços da instância WebHostBuilder e, depois que o aplicativo é criado, você configura o middleware. Os pontos principais a serem observados são a chamada a AddDefaultIdentity para os serviços necessários e as chamadas UseAuthentication e UseAuthorization que adicionam o middleware necessário.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
  app.UseExceptionHandler("/Error");
  // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
  app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

Configurar o Identity na inicialização do aplicativo

// Add framework services.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();
builder.Services.AddMvc();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

É importante que UseAuthentication e UseAuthorization apareçam antes de MapRazorPages. Ao configurar os serviços do Identity, você notará uma chamada para AddDefaultTokenProviders. Isso não tem nada a ver com tokens que podem ser usados para proteger comunicações da Web, mas refere-se a provedores que criam prompts que podem ser enviados aos usuários por SMS ou email para que eles confirmem sua identidade.

Saiba mais sobre como configurar a autenticação de dois fatores e habilitar provedores de logon externo na documentação oficial do ASP.NET Core.

Autenticação

A autenticação é o processo de determinar quem está acessando o sistema. Se você estiver usando o ASP.NET Core Identity e os métodos de configuração mostrados na seção anterior, ele vai configurar automaticamente alguns padrões de autenticação no aplicativo. No entanto, você também pode configurar esses padrões manualmente ou substituir os definidos por AddIdentity. Se você estiver usando o Identity, ele vai configurar a autenticação baseada em cookie como o esquema padrão.

Na autenticação baseada na Web, normalmente até cinco ações podem ser executadas no decorrer da autenticação do cliente de um sistema. Eles são:

  • Autenticar. Usar as informações fornecidas pelo cliente para criar uma identidade a ser usada no aplicativo.
  • Desafio. Essa ação é usada para exigir que o cliente se identifique.
  • Proibir. Informar ao cliente que ele está proibido de executar uma ação.
  • Entrar. Persistir o cliente existente de alguma forma.
  • Sair. Remover o cliente da persistência.

Há várias técnicas comuns para executar a autenticação em aplicativos Web. Elas são chamadas de esquemas. Um determinado esquema definirá ações para algumas das opções acima ou para todas elas. Alguns esquemas dão suporte apenas a um subconjunto de ações e podem exigir um esquema separado para execução das ações sem suporte. Por exemplo, o esquema OIDC (OpenId-Connect) não dá suporte à entrada ou saída, mas normalmente é configurado para usar a autenticação de cookie nessa persistência.

No aplicativo ASP.NET Core, você pode configurar um DefaultAuthenticateScheme bem como esquemas específicos opcionais para cada uma das ações descritas acima. Por exemplo, DefaultChallengeScheme e DefaultForbidScheme. Chamar AddIdentity configura vários aspectos do aplicativo e adiciona muitos serviços necessários. Esta chamada também é incluída para configurar o esquema de autenticação:

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
    options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});

Esses esquemas usam cookies para persistência e redirecionamento a páginas de logon para autenticação por padrão. Esses esquemas são apropriados para aplicativos Web que interagem com usuários por meio de navegadores da Web, mas não são recomendados para APIs. Nesse caso, as APIs normalmente usarão outra forma de autenticação, como tokens de portador JWT.

As APIs da Web são consumidas pelo código, como HttpClient em aplicativos .NET e tipos equivalentes em outras estruturas. Esses clientes esperam uma resposta utilizável de uma chamada à API ou um código de status que indique qual problema ocorreu, quando ocorre algum. Esses clientes não interagem por meio de um navegador e não renderizam nem interagem com nenhum HTML que uma API possa retornar. Portanto, não é apropriado que os pontos de extremidade da API redirecionem os clientes às páginas de logon se eles não forem autenticados. Outro esquema é mais apropriado.

Para configurar a autenticação das APIs, você pode configurar a autenticação da seguinte forma, usada pelo projeto PublicApi no aplicativo de referência eShopOnWeb:

builder.Services
    .AddAuthentication(config =>
    {
      config.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(config =>
    {
        config.RequireHttpsMetadata = false;
        config.SaveToken = true;
        config.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false
        };
    });

Embora seja possível configurar vários esquemas de autenticação diferentes em um só projeto, é muito mais simples configurar um só esquema padrão. Por esse motivo, entre outros, o aplicativo de referência eShopOnWeb separa as APIs no próprio projeto, PublicApi, separado do projeto Web principal que inclui as exibições e os Razor Pages do aplicativo.

Autenticação em aplicativos Blazor

Os aplicativos Blazor Server podem aproveitar os mesmos recursos de autenticação que qualquer outro aplicativo ASP.NET Core. BlazorOs aplicativos WebAssembly não podem usar provedores internos de identidade e autenticação, pois eles são executados no navegador. BlazorOs aplicativos WebAssembly podem armazenar o status de autenticação do usuário localmente e acessar declarações para determinar quais ações os usuários poderão executar. No entanto, todas as verificações de autenticação e autorização devem ser executadas no servidor, independentemente de qualquer lógica implementada dentro do aplicativo BlazorWebAssembly, pois os usuários podem ignorar facilmente o aplicativo e interagir com as APIs diretamente.

Referências – Autenticação

Autorização

A forma mais simples de autorização envolve a restrição do acesso a usuários anônimos. Essa funcionalidade pode ser obtida aplicando o atributo [Authorize] a determinados controladores ou ações. Se funções estiverem sendo usadas, o atributo poderá ser estendido ainda mais para restringir o acesso a usuários que pertencem a determinadas funções, conforme mostrado:

[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{

}

Nesse caso, os usuários que pertencem às funções HRManager e/ou Finance teriam acesso ao SalaryController. Para exigir que um usuário pertença a várias funções (não apenas a uma de várias), aplique o atributo várias vezes, especificando uma função obrigatória por vez.

A especificação de determinados conjuntos de funções como cadeias de caracteres em muitos controladores e ações diferentes pode levar à repetição indesejável. No mínimo, defina constantes para esses literais de cadeia de caracteres e use-as em qualquer lugar em que precise especificar a cadeia de caracteres. Também é possível configurar políticas de autorização que encapsulem regras de autorização e depois especificar a política em vez de funções individuais ao aplicar o atributo [Authorize]:

[Authorize(Policy = "CanViewPrivateReport")]
public IActionResult ExecutiveSalaryReport()
{
    return View();
}

Usando políticas dessa forma, você pode separar os tipos de ações que estão sendo restringidos das funções ou regras específicas que se aplicam a eles. Depois, se você criar uma nova função que precisa ter acesso a determinados recursos, basta atualizar uma política, em vez de atualizar cada lista de funções em cada atributo [Authorize].

Declarações

Declarações são pares de nome e valor que representam as propriedades de um usuário autenticado. Por exemplo, você pode armazenar o número de funcionário dos usuários como uma declaração. Em seguida, as declarações podem ser usadas como parte de políticas de autorização. Você pode criar uma política chamada "EmployeeOnly" que exige a existência de uma declaração chamada "EmployeeNumber", conforme mostrado neste exemplo:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
    });
}

Essa política pode então ser usada com o atributo [Authorize] para proteger qualquer controlador e/ou ação, conforme descrito acima.

Proteger APIs Web

A maioria das APIs Web deve implementar um sistema de autenticação baseada em token. A autenticação de token é sem estado e foi projetada para ser escalonável. Em um sistema de autenticação baseada em token, o cliente deve primeiro ser autenticado no provedor de autenticação. Se ele for bem-sucedido, o cliente receberá um token, que é apenas uma cadeia de caracteres criptograficamente significativa. O formato mais comum de tokens é o Token Web JSON ou JWT (pronunciado como "jot"). Em seguida, quando o cliente precisar emitir uma solicitação para uma API, ele adicionará esse token como um cabeçalho na solicitação. O servidor então validará o token encontrado no cabeçalho da solicitação antes de concluir a solicitação. A Figura 7-4 demonstra esse processo.

TokenAuth

Figura 7-4. Autenticação baseada em token para APIs Web.

Você pode criar seu próprio serviço de autenticação, fazer a integrção ao Azure AD e ao OAuth ou implementar um serviço usando uma ferramenta de software livre, como o IdentityServer.

Os tokens JWT podem inserir declarações sobre o usuário, que podem ser lidas no cliente ou no servidor. Você pode usar uma ferramenta como jwt.io para exibir o conteúdo de um token JWT. Não armazene dados confidenciais, como senhas ou chaves, em tokens JTW, já que o conteúdo pode ser lido com facilidade.

Ao usar tokens JWT com aplicativos SPA ou BlazorWebAssembly, você precisa armazenar o token em algum lugar no cliente e, depois, adicioná-lo a cada chamada à API. Essa atividade normalmente é feita como um cabeçalho, como demonstra o seguinte código:

// AuthService.cs in BlazorAdmin project of eShopOnWeb
private async Task SetAuthorizationHeader()
{
      var token = await GetToken();
      _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}

Após a chamada do método acima, o token será inserido nos cabeçalhos das solicitações feitas com _httpClient, permitindo que a API do lado do servidor autentique e autorize a solicitação.

Segurança personalizada

Cuidado

Como regra geral, evite implementar suas próprias implementações de segurança personalizadas.

Tenha um cuidado especial ao "distribuir sua própria" implementação de criptografia, associação de usuário ou sistema de geração de token. Há várias alternativas comerciais e de software livre disponíveis, que certamente terão uma segurança melhor do que uma implementação personalizada.

Referências – Segurança

Comunicação com o cliente

Além de fornecer páginas e responder a solicitações de dados por meio de APIs Web, os aplicativos ASP.NET Core podem se comunicar diretamente com os clientes conectados. Essa comunicação de saída pode usar uma variedade de tecnologias de transporte, sendo a mais comum o WebSockets. O SignalR do ASP.NET Core é uma biblioteca que simplifica o acréscimo da funcionalidade de comunicação de servidor para cliente em tempo real aos aplicativos. O SignalR é compatível com uma variedade de tecnologias de transporte, incluindo o WebSockets, e abstrai muitos dos detalhes de implementação do desenvolvedor.

A comunicação do cliente em tempo real, seja ela por meio do WebSockets diretamente ou por outras técnicas, é útil em uma variedade de cenários de aplicativos. Alguns exemplos incluem:

  • Aplicativos de sala de chat ao vivo

  • Monitoramento de aplicativos

  • Atualizações de andamento do trabalho

  • Notificações

  • Aplicativos de formulários interativos

Ao desenvolver a comunicação do cliente nos aplicativos, normalmente, há dois componentes:

  • Gerenciador de conexões do lado do servidor (Hub do SignalR, WebSocketManager, WebSocketHandler)

  • Biblioteca do lado do cliente

Os clientes não estão limitados aos navegadores – aplicativos móveis, aplicativos de console e outros aplicativos nativos também podem se comunicar por meio do SignalR/WebSockets. O seguinte programa simples ecoa todo o conteúdo enviado a um aplicativo de chat para o console, como parte de um aplicativo de exemplo WebSocketManager:

public class Program
{
    private static Connection _connection;
    public static void Main(string[] args)
    {
        StartConnectionAsync();
        _connection.On("receiveMessage", (arguments) =>
        {
            Console.WriteLine($"{arguments[0]} said: {arguments[1]}");
        });
        Console.ReadLine();
        StopConnectionAsync();
    }

    public static async Task StartConnectionAsync()
    {
        _connection = new Connection();
        await _connection.StartConnectionAsync("ws://localhost:65110/chat");
    }

    public static async Task StopConnectionAsync()
    {
        await _connection.StopConnectionAsync();
    }
}

Considere maneiras pelas quais os aplicativos se comunicam diretamente com aplicativos cliente e considere se a comunicação em tempo real melhorará a experiência do usuário do aplicativo.

Referências – Comunicação do cliente

Design controlado por domínio: você deve aplicá-lo?

O DDD (Design Controlado por Domínio) é uma abordagem ágil para a criação de software que enfatiza o foco no domínio de negócios. Ele coloca uma grande ênfase na comunicação e interação com especialistas no domínio de negócios que podem relatar para os desenvolvedores como funciona o sistema do mundo real. Por exemplo, se você estiver criando um sistema que manipula mercados de ações, o especialista em domínio poderá ser um corretor de valores experiente. O DDD foi projetado para resolver problemas de negócios grandes e complexos e, geralmente, não é adequado para aplicativos menores e mais simples, pois o investimento no entendimento e na modelagem do domínio não vale a pena.

Ao criar um software seguindo uma abordagem de DDD, sua equipe (incluindo stakeholders não técnicos e colaboradores) deve desenvolver uma linguagem ubíqua para o espaço do problema. Ou seja, a mesma terminologia deve ser usada para o conceito do mundo real que está sendo modelado, o software equivalente e as estruturas que podem existir para persistir o conceito (por exemplo, tabelas de banco de dados). Portanto, os conceitos descritos na linguagem ubíqua devem formar a base do modelo de domínio.

O modelo de domínio consiste em objetos que interagem entre si para representar o comportamento do sistema. Esses objetos podem se enquadrar nas seguintes categorias:

  • Entidades, que representam objetos com um thread de identidade. As entidades costumam ser armazenadas na persistência com uma chave pela qual elas podem ser recuperadas posteriormente.

  • Agregações, que representam grupos de objetos que devem ser persistidos como uma unidade.

  • Objetos de valor, que representam conceitos que podem ser comparados com base na soma de seus valores de propriedade. Por exemplo, DateRange consiste em uma data de início e término.

  • Eventos de domínio, que representam coisas que acontecem no sistema que são de interesse para outras partes do sistema.

Um modelo de domínio DDD deve encapsular um comportamento complexo dentro do modelo. As entidades, em particular, não devem ser apenas coleções de propriedades. Quando o modelo de domínio não tem um comportamento e representa apenas o estado do sistema, ele é chamado de modelo anêmico, o que é indesejável no DDD.

Além desses tipos de modelo, o DDD normalmente emprega uma variedade de padrões:

  • Repositório, para abstrair os detalhes de persistência.

  • Alocador, para encapsular a criação de objetos complexos.

  • Serviços, para encapsular um comportamento complexo e/ou detalhes de implementação de infraestrutura.

  • Comando, para desacoplar a emissão de comandos e executar o comando em si.

  • Especificação, para encapsular os detalhes de consulta.

O DDD também recomenda o uso da Arquitetura Limpa abordada anteriormente, permitindo o acoplamento flexível, o encapsulamento e um código que pode ser verificado com facilidade por meio de testes de unidade.

Quando você deve aplicar o DDD

O DDD é adequado para aplicativos grandes com complexidade comercial (não apenas técnica) significativa. O aplicativo deve exigir o conhecimento de especialistas no domínio. Deve haver um comportamento significativo no próprio modelo de domínio, que representa as regras de negócio e as interações, além de simplesmente armazenar e recuperar o estado atual de vários registros de armazenamentos de dados.

Quando você não deve aplicar o DDD

O DDD envolve investimentos em modelagem, arquitetura e comunicação que não podem ser garantidos para aplicativos menores ou aplicativos que são essencialmente apenas CRUD (criar/ler/atualizar/excluir). Caso você opte por abordar seu aplicativo seguindo o DDD, mas descubra que o domínio tem um modelo anêmico sem nenhum comportamento, talvez você precise repensar sua abordagem. O aplicativo pode não precisar do DDD ou você pode precisar de assistência na refatoração do aplicativo para encapsular a lógica de negócios no modelo de domínio, em vez de na interface do usuário ou no banco de dados.

Uma abordagem híbrida é usar o DDD somente para as áreas transacionais ou mais complexas do aplicativo, mas não para as partes CRUD ou somente leitura mais simples do aplicativo. Por exemplo, você não precisará das restrições de uma agregação se estiver consultando dados para exibir um relatório ou para visualizar dados de um dashboard. É perfeitamente aceitável ter um modelo de leitura mais simples e separado para esses requisitos.

Referências – Design Controlado por Domínio

Implantação

Há algumas etapas envolvidas no processo de implantação do aplicativo ASP.NET Core, independentemente do local em que ele será hospedado. A primeira etapa é publicar o aplicativo, que pode ser feito usando o comando dotnet publish da CLI. Essa etapa compilará o aplicativo e posicionará todos os arquivos necessários para executar o aplicativo em uma pasta designada. Quando você faz a implantação por meio do Visual Studio, esta etapa é executada automaticamente para você. A pasta de publicação contém arquivos .exe e .dll para o aplicativo e suas dependências. Um aplicativo autossuficiente também incluirá uma versão do runtime do .NET. Os aplicativos ASP.NET Core também incluirão arquivos de configuração, ativos de cliente estático e exibições do MVC.

Os aplicativos ASP.NET Core são aplicativos de console que devem ser iniciados quando o servidor é inicializado e reiniciados quando há falhas no aplicativo (ou no servidor). Um gerenciador de processos pode ser usado para automatizar esse processo. Os gerenciadores de processos mais comuns para o ASP.NET Core são o Nginx e o Apache no Linux e o IIS ou o Serviço Windows no Windows.

Além de usar um gerenciador de processos, os aplicativos ASP.NET Core podem usar um servidor proxy reverso. Um servidor proxy reverso recebe solicitações HTTP da Internet e as encaminha para o Kestrel após algum tratamento preliminar. Os servidores proxy reversos fornecem uma camada de segurança para o aplicativo. O Kestrel também não dá suporte à hospedagem de vários aplicativos na mesma porta e, portanto, técnicas como cabeçalhos de host não podem ser usadas com ele para habilitar a hospedagem de vários aplicativos na mesma porta e endereço IP.

Kestrel para a Internet

Figura 7-5. ASP.NET hospedado no Kestrel atrás de um servidor proxy reverso

Outro cenário em que um proxy reverso pode ser útil é para proteger vários aplicativos usando SSL/HTTPS. Nesse caso, somente o proxy inverso precisa ter o SSL configurado. A comunicação entre o servidor proxy reverso e o Kestrel pode ocorrer por HTTP, conforme mostrado na Figura 7-6.

ASP.NET hospedado atrás de um servidor proxy reverso protegido por HTTPS

Figura 7-6. ASP.NET hospedado atrás de um servidor proxy reverso protegido por HTTPS

Uma abordagem cada vez mais popular é hospedar o aplicativo ASP.NET Core em um contêiner do Docker, que pode ser então hospedado localmente ou implantado no Azure para a hospedagem baseada em nuvem. O contêiner do Docker pode conter o código do aplicativo, em execução no Kestrel, e ser implantado atrás de um servidor proxy reverso, conforme mostrado acima.

Caso esteja hospedando seu aplicativo no Azure, use o Gateway de Aplicativo do Microsoft Azure como uma solução de virtualização dedicada para fornecer vários serviços. Além de atuar como um proxy reverso para aplicativos individuais, o Gateway de Aplicativo também pode oferecer os seguintes recursos:

  • Balanceamento de carga HTTP

  • Descarregamento SSL (SSL apenas para a Internet)

  • SSL de ponta a ponta

  • Roteamento de vários sites (consolide até 20 sites em um único Gateway de Aplicativo)

  • Firewall do aplicativo Web

  • Suporte do WebSockets

  • Diagnóstico avançado

Saiba mais sobre as opções de implantação do Azure no Capítulo 10.

Referências – Implantação