Partilhar via


Desenvolva aplicativos MVC ASP.NET Core

Gorjeta

Este conteúdo é um excerto do eBook, Architect Modern Web Applications with ASP.NET Core e Azure, disponível no .NET Docs ou como um PDF transferível gratuito que pode ser lido offline.

Arquitete aplicativos Web modernos com miniatura da capa do ASP.NET Core e do eBook do Azure.

"Não é importante acertar na primeira vez. É de vital importância acertar da última vez." - Andrew Hunt e David Thomas

ASP.NET Core é uma estrutura multiplataforma e de código aberto para a criação de aplicações web modernas otimizadas para a nuvem. ASP.NET aplicativos principais são leves e modulares, com suporte integrado para injeção de dependência, permitindo maior capacidade de teste e manutenção. Combinado com o MVC, que suporta a criação de APIs da Web modernas, além de aplicativos baseados em visualização, o ASP.NET Core é uma estrutura poderosa com a qual criar aplicativos Web corporativos.

Páginas MVC e Razor

ASP.NET Core MVC oferece muitos recursos que são úteis para criar APIs e aplicativos baseados na Web. O termo MVC significa "Model-View-Controller", um padrão de interface do usuário que divide as responsabilidades 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 Razor Pages.

As Páginas Razor são incorporadas ASP.NET MVC Core e usam os mesmos recursos para roteamento, vinculação de modelo, filtros, autorização, etc. No entanto, em vez de ter pastas e arquivos separados para controladores, modelos, visualizações, etc. e usar roteamento baseado em atributos, Razor Pages são colocados em uma única pasta ("/Pages"), rotear com base em sua localização relativa nesta pasta e lidar com solicitações com manipuladores em vez de ações do controlador. Como resultado, ao trabalhar com o Razor Pages, todos os arquivos e classes que você precisa normalmente são colocalizados, não espalhados por todo o projeto da Web.

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

Ao criar um novo ASP.NET Core App, você deve ter um plano em mente para o tipo de aplicativo que deseja criar. Ao criar um novo projeto, em seu IDE ou usando o dotnet new comando CLI, você escolherá entre vários modelos. Os modelos de projeto mais comuns são Empty, Web API, Web App e Web App (Model-View-Controller). Embora você só possa tomar essa decisão quando criar um projeto pela primeira vez, não é uma decisão irrevogável. O projeto de API da Web usa controladores Model-View-Controller padrão – ele simplesmente não possui Views por padrão. Da mesma forma, o modelo de Aplicativo Web padrão usa Páginas Razor e, portanto, também não tem uma pasta Exibições. Você pode adicionar uma pasta Views a esses projetos posteriormente para oferecer suporte ao comportamento baseado em exibição. Os projetos de API da Web e Model-View-Controller não incluem uma pasta Pages por padrão, mas você pode adicionar uma posteriormente para oferecer suporte ao comportamento baseado em Razor Pages. Você pode pensar nesses três modelos como suportando três tipos diferentes de interação padrão do usuário: dados (API da Web), baseados em página e baseados em exibição. No entanto, você pode misturar e combinar qualquer um ou todos esses modelos dentro de um único projeto, se desejar.

Porquê Razor Pages?

Razor Pages é a abordagem padrão para novos aplicativos Web no Visual Studio. O Razor Pages oferece uma maneira mais simples de criar recursos de aplicativos baseados em página, como formulários que não sejam SPA. Usando 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 muitas exibições diferentes. Isso resultou em mais complexidade e, muitas vezes, resultou em controladores que não seguiram o Princípio da Responsabilidade Única ou os Princípios Abertos/Fechados de forma eficaz. O Razor Pages resolve esse problema encapsulando a lógica do lado do servidor para uma determinada "página" lógica em um aplicativo da Web com sua marcação Razor. Uma página Razor que não tem lógica do lado do servidor só pode consistir em um arquivo Razor (por exemplo, "Index.cshtml"). No entanto, a maioria das Páginas Razor não triviais terá uma classe de modelo de página associada, que por convenção é nomeada da mesma forma que o arquivo Razor com uma extensão ".cs" (por exemplo, "Index.cshtml.cs").

O modelo de página de um Razor Page combina as responsabilidades de um controlador MVC e um viewmodel. Em vez de lidar com solicitações com métodos de ação do controlador, manipuladores de modelo de página como "OnGet()" são executados, renderizando sua página associada por padrão. O Razor Pages simplifica o processo de criação de páginas individuais em um aplicativo ASP.NET Core, ao mesmo tempo em que fornece todos os recursos arquitetônicos do ASP.NET Core MVC. Eles são uma boa opção padrão para novas funcionalidades baseadas em página.

Quando usar o MVC

Se você estiver criando APIs da Web, o padrão MVC fará mais sentido do que tentar usar o Razor Pages. Se o seu projeto irá expor apenas pontos de extremidade de API Web, você deve idealmente começar a partir do modelo de projeto de API Web. Caso contrário, é fácil adicionar controladores e pontos de extremidade de API associados a qualquer aplicativo ASP.NET Core. Use a abordagem MVC baseada em visualização se estiver migrando um aplicativo existente de ASP.NET MVC 5 ou anterior para ASP.NET MVC principal e quiser fazer isso com o mínimo de esforço. Depois de fazer a migração inicial, você pode avaliar se faz sentido adotar o Razor Pages para novos recursos ou até mesmo como uma migração por atacado. Para obter mais informações sobre como portar aplicativos .NET 4.x para .NET 8, consulte Portando aplicativos ASP.NET existentes para ASP.NET eBook principal.

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

Mapeando solicitações para respostas

Em sua essência, os aplicativos ASP.NET Core mapeiam solicitações de entrada para respostas de saída. Em um nível baixo, esse mapeamento é feito com middleware, e aplicativos e microsserviços simples ASP.NET Core podem ser compostos apenas por middleware personalizado. Ao usar ASP.NET Core MVC, você pode trabalhar em um nível um pouco mais alto, 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 for encontrada, o método de ação associado (pertencente a um controlador) será chamado para lidar com a solicitação. Se nenhuma rota correspondente for encontrada, um manipulador de erros (neste caso, retornando um resultado NotFound) será chamado.

ASP.NET aplicativos MVC principais podem usar rotas convencionais, rotas de atributos ou ambos. As rotas convencionais são definidas em código, especificando convenções de roteamento usando sintaxe como no exemplo abaixo:

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

Neste exemplo, uma rota chamada "default" foi adicionada à tabela de roteamento. Ele define um modelo de rota com espaços reservados para controller, actione id. Os controller espaços reservados 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 aqui definida estabelece que a primeira parte de uma solicitação deve corresponder ao nome do controlador, a segunda parte à ação e, se necessário, uma terceira parte representará um parâmetro ID. As rotas convencionais geralmente são definidas em um local para o aplicativo, como em Program.cs onde o pipeline de middleware de solicitação está configurado.

As rotas de atributos são aplicadas a controladores e ações diretamente, em vez de especificadas globalmente. Essa abordagem tem a vantagem de torná-los muito mais detetáveis quando você está olhando para um método específico, mas significa que as informações de roteamento não são mantidas em um único lugar no aplicativo. Com rotas de atributo, você pode facilmente especificar várias rotas para uma determinada ação, bem como 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 em [HttpGet] e atributos semelhantes, evitando a necessidade de adicionar atributos [Route] separados. As rotas de atributos também podem usar tokens para reduzir a necessidade de repetir nomes de controladores ou ações, conforme mostrado abaixo:

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

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

@page "{id:int}"

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

/Products/123

Depois que uma determinada solicitação tiver sido correspondida a uma rota, mas antes que o método de ação seja chamado, ASP.NET MVC principal executará a vinculação de modelo e a validação de modelo na solicitação. A vinculação de modelo é responsável por converter 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 action espera um int id parâmetro, a vinculação de modelo tentará fornecer esse parâmetro a partir de um valor fornecido como parte da solicitação. Para fazer isso, a vinculação de modelo procura valores em um formulário postado, valores na própria rota e valores de cadeia de caracteres de consulta. Supondo que um id valor seja encontrado, ele será convertido em um inteiro antes de ser passado para o método de ação.

Depois de vincular o modelo, mas antes de chamar o método de ação, ocorre a validação do modelo. A validação de modelo usa atributos opcionais no tipo de modelo e pode ajudar a garantir que o objeto de modelo fornecido esteja em conformidade com determinados requisitos de dados. Certos valores podem ser especificados conforme necessário, ou limitados a um determinado comprimento ou intervalo numérico, etc. Se os atributos de validação forem especificados, mas o modelo não estiver em conformidade com seus requisitos, a propriedade ModelState.IsValid será false, e o conjunto de regras de validação com falha estará disponível para enviar ao cliente que faz a solicitação.

Se você estiver usando a validação de modelo, certifique-se de sempre verificar se o modelo é válido antes de executar qualquer comando de alteração de estado, para garantir que seu aplicativo não esteja 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. ASP.NET filtros MVC principais oferecem uma maneira de intercetar grupos de solicitações, para que políticas comuns e preocupações transversais possam ser aplicadas de forma direcionada. Os filtros podem ser aplicados a ações individuais, controladores inteiros ou globalmente para um aplicativo.

Para APIs da Web, o ASP.NET Core MVC oferece suporte à 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 suportado. Esse recurso permite que a mesma API seja usada por vários clientes com diferentes requisitos de formato de dados.

Os projetos de API da Web devem considerar o uso do [ApiController] atributo, que pode ser aplicado a controladores individuais, a uma classe de controlador base ou a todo o assembly. Este atributo adiciona a verificação automática de validação de modelo e 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 atributo, em vez de usar uma rota convencional, e retorna informações ProblemDetails mais detalhadas em resposta a erros.

Manter os controladores sob controlo

Para aplicativos baseados em página, o Razor Pages faz um ótimo trabalho para evitar que os controladores fiquem muito grandes. Cada página individual recebe seus próprios arquivos e classes dedicadas apenas ao(s) seu(s) manipulador(es). Antes da introdução do Razor Pages, muitos aplicativos centrados na visualização tinham grandes classes de controladores responsáveis por muitas ações e visualizações diferentes. Estas classes cresceriam naturalmente para terem muitas responsabilidades e dependências, tornando-as mais difíceis de manter. Se você achar que seus controladores baseados em visualização estão crescendo demais, considere refatoração-los para usar o Razor Pages ou introduzir um padrão como um mediador.

O padrão de design do mediador é usado para reduzir o acoplamento entre classes, permitindo a comunicação entre elas. Em ASP.NET aplicativos MVC principais, esse padrão é freqüentemente empregado para dividir controladores em partes menores usando manipuladores para fazer o trabalho de métodos de ação. O popular pacote MediatR NuGet é frequentemente usado para realizar isso. Normalmente, os controladores incluem muitos métodos de ação diferentes, cada um dos quais pode exigir determinadas dependências. O conjunto de todas as dependências exigidas por qualquer ação deve ser passado para o construtor do controlador. Ao usar o MediatR, a única dependência que um controlador normalmente terá é uma instância do mediador. Cada ação usa a instância do mediador para enviar uma mensagem, que é processada por um manipulador. O manipulador é específico para uma única ação e, portanto, só precisa das dependências exigidas por essa ação. Um exemplo de um controlador usando MediatR é 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
}

MyOrders Na ação, a chamada para Send uma GetMyOrders mensagem é tratada por esta 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 sejam muito menores e focados principalmente no roteamento e na vinculação de modelos, 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 alcançada sem o MediatR usando o pacote NuGet ApiEndpoints, que tenta trazer para os controladores de API os mesmos benefícios que o Razor Pages traz para os controladores baseados em visualização.

Referências – Mapeando solicitações para respostas

Trabalhando com dependências

ASP.NET Core tem suporte integrado e faz uso interno de uma técnica conhecida como injeção de dependência. A injeção de dependência é uma técnica que permite o acoplamento solto entre diferentes partes de uma aplicação. O acoplamento mais solto é desejável porque facilita o isolamento de partes da aplicação, permitindo testes ou substituição. Isso também torna menos provável que uma alteração em uma parte do aplicativo tenha um impacto inesperado em outro lugar no aplicativo. A injeção de dependência baseia-se no princípio da inversão da dependência e é frequentemente fundamental para alcançar o princípio aberto/fechado. Ao avaliar como seu aplicativo funciona com suas dependências, cuidado com o cheiro do código de aderência estática e lembre-se do aforismo "new is glue".

O cling estático ocorre quando suas classes fazem chamadas para 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, seu método está firmemente acoplado ao banco de dados. Qualquer coisa que quebre essa chamada de banco de dados quebrará seu método. Testar tais métodos é notoriamente difícil, uma vez que tais testes exigem bibliotecas comerciais simuladas para simular as chamadas estáticas, ou só podem ser testados com um banco de dados de teste no lugar. As chamadas estáticas que não dependem da infraestrutura, especialmente aquelas chamadas que são completamente sem monitoração de estado, são boas para chamar e não têm impacto no acoplamento ou na capacidade de teste (além do código de acoplamento à própria chamada estática).

Muitos desenvolvedores entendem os riscos do bloqueio estático e do estado global, mas ainda associam firmemente seu código a implementações específicas por meio de instanciação direta. "Novo é cola" pretende ser um lembrete desse acoplamento, e não uma condenação geral do uso da new palavra-chave. Assim como acontece com chamadas de método estático, novas instâncias de tipos que não têm dependências externas normalmente não associam o código aos detalhes da implementação ou dificultam o teste. Mas cada vez que uma classe é instanciada, reserve apenas um breve momento para considerar se faz sentido codificar essa instância específica naquele local específico ou se seria um design melhor solicitar essa instância como uma dependência.

Declarar suas dependências

ASP.NET Core é construído em torno de ter métodos e classes declarando suas dependências, solicitando-as como argumentos. ASP.NET aplicativos são normalmente configurados em Program.cs ou em uma Startup classe.

Nota

Configurar aplicativos completamente em Program.cs é a abordagem padrão para aplicativos .NET 6 (e posteriores) e Visual Studio 2022. Os modelos de projeto foram atualizados para ajudá-lo a começar com essa nova abordagem. ASP.NET Os projetos principais ainda podem usar uma Startup classe, se desejado.

Configurar serviços no Program.cs

Para aplicativos muito simples, você pode conectar dependências diretamente em Program.cs arquivo usando um WebApplicationBuilderarquivo . Uma vez que todos os serviços necessários tenham sido adicionados, o construtor é 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 no Startup.cs

O próprio Startup.cs é configurado para suportar a injeção de dependência em vários pontos. Se você estiver usando uma Startup classe, você pode dar-lhe um construtor e ele pode solicitar dependências através dele, assim:

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 Startup classe é interessante na medida em que não há requisitos de tipo explícitos para ela. Ele não herda de uma classe base especial Startup , nem implementa nenhuma interface em particular. Você pode dar a ele um construtor, ou não, e você pode especificar quantos parâmetros no construtor quiser. Quando o host da Web que você configurou para seu aplicativo for iniciado, ele chamará a classe (se você tiver dito para usar uma) e usará a Startup injeção de dependência para preencher quaisquer dependências que a Startup classe requeira. É claro que, se você solicitar parâmetros que não estejam configurados no contêiner de serviços usado pelo ASP.NET Core, obterá uma exceção, mas desde que mantenha as dependências que o contêiner conhece, poderá solicitar o que quiser.

A injeção de dependência é incorporada em seus aplicativos ASP.NET Core desde o início, quando você cria a instância de inicialização. Não para por aí para a classe Startup. Você também pode solicitar dependências no Configure método:

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

}

O método ConfigureServices é a exceção a esse comportamento; ele deve tomar apenas um parâmetro do tipo IServiceCollection. Ele realmente não precisa suportar a injeção de dependência, uma vez que, 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 através do IServiceCollection parâmetro. Assim, você pode trabalhar com dependências definidas na coleção ASP.NET Core services em cada parte da Startup classe, solicitando o serviço necessário como parâmetro ou trabalhando com o IServiceCollection in ConfigureServices.

Nota

Se você precisar garantir que determinados serviços estejam disponíveis para sua Startup classe, você pode configurá-los usando um IWebHostBuilder e seu ConfigureServices método dentro da CreateDefaultBuilder chamada.

A classe Startup é um modelo de como você deve estruturar outras partes do seu aplicativo ASP.NET Core, de controladores a middleware, filtros e seus próprios serviços. Em cada caso, você deve seguir o Princípio de Dependências Explícitas, solicitando suas dependências em vez de criá-las diretamente e aproveitando a injeção de dependência em todo o aplicativo. Tenha cuidado com onde e como você instancia diretamente implementações, especialmente serviços e objetos que funcionam com infraestrutura ou têm efeitos colaterais. Prefira trabalhar com abstrações definidas no núcleo do aplicativo e passadas como argumentos para codificar referências a tipos de implementação específicos.

Estruturação da aplicação

As aplicações monolíticas normalmente têm um único ponto de entrada. No caso de uma aplicação 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 deva consistir em apenas um único projeto. É útil dividir o aplicativo em diferentes camadas para seguir a separação de preocupações. Uma vez dividido em camadas, é útil ir além das pastas para separar projetos, o que pode ajudar a obter um melhor encapsulamento. A melhor abordagem para atingir esses objetivos com um aplicativo ASP.NET Core é uma variação da Arquitetura Limpa discutida no capítulo 5. Seguindo essa abordagem, a solução do aplicativo compreenderá bibliotecas separadas para a interface do usuário, infraestrutura e ApplicationCore.

Além desses projetos, projetos de teste separados também estão incluídos (os testes são discutidos no Capítulo 9).

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

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

O projeto ASP.NET Core UI é responsável por quaisquer preocupações no nível da interface do usuário, mas não deve incluir detalhes de lógica de negócios ou infraestrutura. Na verdade, o ideal seria que nem sequer dependesse do projeto de infraestrutura, o que ajudará a garantir que nenhuma dependência entre os dois projetos seja introduzida acidentalmente. Isso pode ser conseguido usando um contêiner DI de terceiros como o Autofac, que permite definir regras DI em classes de módulo em cada projeto.

Outra abordagem para dissociar o aplicativo dos detalhes da implementação é fazer com que o aplicativo chame microsserviços, talvez implantados em contêineres individuais do Docker. Isso proporciona uma separação ainda maior de preocupações e dissociação do que alavancar a DI entre dois projetos, mas tem complexidade adicional.

Organização de recursos

Por padrão, os aplicativos ASP.NET Core organizam sua estrutura de pastas para incluir Controllers e Views, e frequentemente ViewModels. O código do lado do cliente para suportar essas estruturas do lado do servidor normalmente é armazenado separadamente na pasta wwwroot. No entanto, aplicativos grandes podem encontrar problemas com essa organização, uma vez que trabalhar em qualquer recurso geralmente requer saltar entre essas pastas. Isso fica cada vez mais difícil à medida que o número de arquivos e subpastas em cada pasta cresce, 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 chamado de pastas de recursos ou fatias de recursos (consulte também: Fatias verticais).

ASP.NET Core MVC suporta Áreas para esta finalidade. Usando áreas, você pode criar conjuntos separados de pastas Controladores e Exibições (bem como quaisquer modelos associados) em cada pasta Área. A Figura 7-1 mostra um exemplo de estrutura de pastas, usando Areas.

Organização da Área de Amostra

Figura 7-1. Organização da Área de Amostra

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

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

Você também precisa adicionar suporte de área às suas 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 permitiria que você tivesse pastas de recursos que não incluíssem pastas separadas para Visualizações, Controladores, etc., mantendo a hierarquia mais plana e facilitando a visualização de todos os arquivos relacionados em um único lugar para cada recurso. Para APIs, as pastas podem ser usadas para substituir controladores, e cada pasta pode conter todos os pontos de extremidade da API e seus DTOs associados.

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 obterá automaticamente o nome do recurso para um determinado controlador com base em seu namespace (que normalmente 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;
    }
}

Em seguida, especifique essa convenção como uma opção ao adicionar suporte para MVC ao seu 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()));

ASP.NET Core MVC também usa uma convenção para localizar visualizações. Você pode substituí-lo por uma convenção personalizada para que os modos de exibição estejam localizados em suas pastas de recursos (usando o nome do recurso fornecido pela FeatureConvention, acima). Você pode saber mais sobre essa abordagem e baixar um exemplo de trabalho do artigo da MSDN Magazine, Feature Slices for ASP.NET Core MVC.

APIs e Blazor aplicativos

Se seu aplicativo incluir um conjunto de APIs da Web, que devem ser protegidas, essas APIs devem idealmente ser configuradas como um projeto separado do seu aplicativo View ou Razor Pages. Separar APIs, especialmente APIs públicas, do seu aplicativo Web do lado do servidor tem vários benefícios. Esses aplicativos geralmente terão características exclusivas de implantação e carga. Também é muito provável que adotem diferentes mecanismos de segurança, com aplicativos baseados em formulários padrão aproveitando a autenticação baseada em cookies e APIs provavelmente usando autenticação baseada em token.

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

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

Pode-se perguntar, por que adicionar um projeto separado BlazorShared quando já existe um projeto comum ApplicationCore que poderia ser usado para compartilhar quaisquer tipos exigidos por ambos PublicApi e BlazorAdmin? A resposta é que este projeto inclui toda a lógica de negócios do aplicativo e, portanto, é muito maior do que o necessário e também muito mais provável de precisar ser mantido seguro no servidor. Lembre-se de que qualquer biblioteca referenciada por BlazorAdmin será baixada para os navegadores dos usuários quando eles carregarem o Blazor aplicativo.

Dependendo se alguém estiver usando o padrão Backends-For-Frontends (BFF), as APIs consumidas pelo aplicativo podem não compartilhar seus tipos 100% com Blazoro BlazorWebAssembly . Em particular, uma API pública destinada a ser consumida por muitos clientes diferentes pode definir seus próprios tipos de solicitação e resultado, em vez de compartilhá-los em um projeto compartilhado específico do cliente. No exemplo eShopOnWeb, a suposição está sendo feita de que o PublicApi projeto está, de fato, hospedando uma API pública, portanto, nem todos os seus tipos de solicitação e resposta vêm do BlazorShared projeto.

Questões transversais

À medida que os aplicativos crescem, torna-se cada vez mais importante considerar as preocupações transversais para eliminar a duplicação e manter a consistência. Alguns exemplos de preocupações transversais em aplicativos ASP.NET Core são autenticação, regras de validação de modelo, cache de saída e tratamento de erros, embora existam muitos outros. ASP.NET filtros MVC principais permitem executar código antes ou depois de determinadas etapas no pipeline de processamento de solicitações. Por exemplo, um filtro pode ser executado antes e depois da vinculação do modelo, antes e depois de uma ação ou antes e depois do 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 a execução da solicitação flui através de filtros, se configurada.

A solicitação é processada por meio de Filtros de Autorização, Filtros de Recursos, Vinculação de Modelo, Filtros de Ação, Execução de Ação e Conversão de Resultados de Ações, Filtros de Exceção, Filtros de Resultados e Execução de Resultados. Na saída, a solicitação é processada apenas por Filtros de Resultados e Filtros de Recursos antes de se tornar uma resposta enviada ao cliente.

Figura 7-2. Execução de pedidos através de filtros e pipeline de pedidos.

Os filtros geralmente são implementados como atributos, para que você possa aplicá-los a controladores ou ações (ou até mesmo globalmente). Quando adicionados dessa maneira, os filtros especificados no nível de ação substituem ou se baseiam nos filtros especificados no nível do controlador, que substituem os filtros globais. Por exemplo, o [Route] atributo pode ser usado para construir 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 demonstra o exemplo a seguir:

[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 [AllowAnonymous] filtro (atributo) para substituir o conjunto de filtros Authorize no nível do controlador. A ForgotPassword ação (e qualquer outra ação na classe que não tenha um atributo AllowAnonymous) exigirá uma solicitação autenticada.

Os filtros podem ser usados para eliminar a duplicação na forma de políticas comuns de tratamento de erros para APIs. Por exemplo, uma política de API típica é retornar uma resposta NotFound para solicitações que fazem referência a chaves que não existem e uma BadRequest resposta se a validação do modelo falhar. O exemplo a seguir 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 seus métodos de ação fiquem confusos com código condicional como este. Em vez disso, puxe as políticas para 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 para a API, pode ser substituída pelo seguinte atributo:

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

Você pode adicionar o ValidateModelAttribute ao seu projeto como uma dependência do NuGet incluindo o pacote Ardalis.ValidateModel . Para APIs, você pode usar o ApiController atributo para impor esse comportamento sem a necessidade de um filtro separado ValidateModel .

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

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

Você pode ler mais sobre como implementar filtros e baixar um exemplo de trabalho do artigo da MSDN Magazine, Real-World ASP.NET Core MVC Filters.

Se você achar que tem várias respostas comuns de APIs com base em cenários comuns, como erros de validação (Solicitação incorreta), recurso não encontrado e erros de servidor, considere usar uma abstração de resultado . A abstração de resultado seria retornada por serviços consumidos por pontos de extremidade de API, e a ação do controlador ou ponto de extremidade usaria um filtro para traduzi-los em IActionResults.

Referências – Estruturação de aplicações

Segurança

A proteção de aplicações Web é um tópico extenso, com muitas considerações. Em seu nível mais básico, a segurança envolve garantir que você saiba de quem uma determinada solicitação está vindo e, em seguida, garantir que a solicitação só tenha acesso aos recursos que deveria. A autenticação é o processo de comparar as credenciais fornecidas com uma solicitação com aquelas em um armazenamento de dados confiável, para ver se a solicitação deve ser tratada como proveniente de uma entidade conhecida. A 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 escutas de terceiros, para as quais você deve, pelo menos , garantir que o SSL seja usado pelo seu aplicativo.

Identidade

ASP.NET Core Identity é um sistema de associação que você pode usar para suportar a funcionalidade de login para seu aplicativo. Ele tem suporte para contas de usuário locais, bem como suporte de provedor de login externo de provedores como Conta da Microsoft, Twitter, Facebook, Google e muito mais. Além do ASP.NET Core Identity, seu aplicativo pode usar a autenticação do Windows ou um provedor de identidade de terceiros, como o Identity Server.

ASP.NET Identidade Principal será incluída em novos modelos de projeto se a opção Contas de Usuário Individual estiver selecionada. Este modelo inclui suporte para registo, login, inícios de sessão externos, palavras-passe esquecidas e funcionalidades adicionais.

Selecione Contas de Usuário Individuais para ter a Identidade pré-configurada

Figura 7-3. Selecione Contas de Usuário Individuais para ter a Identidade pré-configurada.

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

Configurar identidade no Program.cs

No Program.cs, você configura serviços a partir da instância e, depois WebHostBuilder que o aplicativo é criado, configura seu middleware. Os pontos-chave a serem observados são a chamada para AddDefaultIdentity os serviços necessários e as UseAuthentication chamadas 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();

Configurando a identidade 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 isso UseAuthentication e UseAuthorization aparecer antes MapRazorPages. Ao configurar os serviços de identidade, 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 se refere a provedores que criam prompts que podem ser enviados aos usuários por SMS ou e-mail para que eles confirmem sua identidade.

Você pode saber mais sobre como configurar a autenticação de dois fatores e habilitar provedores de login externos nos documentos oficiais do ASP.NET Core.

Autenticação

A autenticação é o processo de determinar quem está acessando o sistema. Se você estiver usando ASP.NET Identidade Principal e os métodos de configuração mostrados na seção anterior, ele 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 configurará a autenticação baseada em cookies como o esquema padrão.

Na autenticação baseada na Web, normalmente há até cinco ações que podem ser executadas durante a autenticação de um cliente de um sistema. São as seguintes:

  • Autenticar. Use as informações fornecidas pelo cliente para criar uma identidade para ele usar dentro do aplicativo.
  • Desafio. Esta ação é usada para exigir que o cliente se identifique.
  • Proibir. Informar o cliente que está proibido de realizar uma ação.
  • Iniciar sessão. Persista o cliente existente de alguma forma.
  • Sair. Remova o cliente da persistência.

Há várias técnicas comuns para executar a autenticação em aplicativos Web. São os chamados regimes. Um determinado esquema definirá ações para algumas ou todas as opções acima. Alguns regimes apoiam apenas um subconjunto de ações e podem exigir um regime separado para executar as que não apoiam. Por exemplo, o esquema OpenId-Connect (OIDC) não suporta Sign-in ou Sign-out, mas normalmente é configurado para usar a autenticação de cookie para essa persistência.

Em seu aplicativo ASP.NET Core, você pode configurar um esquema específico opcional para cada uma DefaultAuthenticateScheme das ações descritas acima. Por exemplo, DefaultChallengeScheme e DefaultForbidScheme. A chamada AddIdentity configura vários aspetos do aplicativo e adiciona muitos serviços necessários. Ele também inclui esta chamada 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 para páginas de login para autenticação por padrão. Esses esquemas são apropriados para aplicativos da Web que interagem com os usuários por meio de navegadores da Web, mas não são recomendados para APIs. Em vez disso, as APIs normalmente usarão outra forma de autenticação, como tokens de portador JWT.

As APIs da Web são consumidas por código, como HttpClient em aplicativos .NET e tipos equivalentes em outras estruturas. Esses clientes esperam uma resposta utilizável de uma chamada de API ou um código de status indicando qual, se houver, problema ocorreu. Esses clientes não estão interagindo por meio de um navegador e não processam ou interagem com qualquer HTML que uma API possa retornar. Assim, não é apropriado que os pontos de extremidade da API redirecionem seus clientes para páginas de login se eles não forem autenticados. Outro esquema é mais adequado.

Para configurar a autenticação para APIs, você pode configurar a PublicApi autenticação como a seguinte, usada pelo projeto 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 dentro de um único projeto, é muito mais simples configurar um único esquema padrão. Por esta razão, entre outros, o aplicativo de referência eShopOnWeb separa suas APIs em seu próprio projeto, PublicApiseparado do projeto principal Web que inclui as visualizações do aplicativo e Razor Pages.

Autenticação em Blazor aplicativos

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

Referências – Autenticação

Autorização

A forma mais simples de autorização envolve restringir o acesso a usuários anônimos. Essa funcionalidade pode ser alcançada aplicando o [Authorize] atributo a determinados controladores ou ações. Se as funções estiverem sendo usadas, o atributo poderá ser estendido 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 HRManager usuários pertencentes às funções ou Finance (ou ambas) teriam acesso ao SalaryController. Para exigir que um usuário pertença a várias funções (não apenas a uma das várias), você pode aplicar o atributo várias vezes, especificando uma função necessária a cada vez.

Especificar certos conjuntos de funções como cadeias de caracteres em muitos controladores e ações diferentes pode levar a uma repetição indesejável. No mínimo, defina constantes para esses literais de cadeia de caracteres e use as constantes em qualquer lugar que você precise especificar a cadeia de caracteres. Você também pode configurar políticas de autorização, que encapsulam regras de autorização e, em seguida, especificar a política em vez de funções individuais ao aplicar o [Authorize] atributo:

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

Usando políticas dessa maneira, você pode separar os tipos de ações que estão sendo restritas das funções ou regras específicas que se aplicam a ela. Mais tarde, se você criar uma nova função que precise ter acesso a determinados recursos, poderá apenas atualizar uma política, em vez de atualizar todas as listas de funções em cada [Authorize] atributo.

Pedidos

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

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

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

Protegendo APIs da Web

A maioria das APIs da Web deve implementar um sistema de autenticação baseado em token. A autenticação de token é sem monitoração de estado e projetada para ser escalável. Em um sistema de autenticação baseado em token, o cliente deve primeiro autenticar com o provedor de autenticação. Se for bem-sucedido, o cliente recebe um token, que é simplesmente uma cadeia de caracteres criptograficamente significativa. O formato mais comum para tokens é JSON Web Token, ou JWT (muitas vezes pronunciado "jot"). Quando o cliente precisa emitir uma solicitação para uma API, ele adiciona esse token como um cabeçalho na solicitação. Em seguida, o servidor valida 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 da Web.

Você pode criar seu próprio serviço de autenticação, integrar com o Azure AD e OAuth ou implementar um serviço usando uma ferramenta de código aberto como o IdentityServer.

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

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

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

Depois de chamar o método acima, as solicitações feitas com o _httpClient terão o token incorporado nos cabeçalhos da solicitação, permitindo que a API do lado do servidor autentique e autorize a solicitação.

Segurança Personalizada

Atenção

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

Tenha especial cuidado com a implementação "rolando sua própria" de criptografia, associação de usuário ou sistema de geração de tokens. Existem muitas alternativas comerciais e de código aberto disponíveis, que quase certamente terão melhor segurança do que uma implementação personalizada.

Referências – Segurança

Comunicação com o cliente

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

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

  • Aplicativos de sala de bate-papo ao vivo

  • Aplicações de monitorização

  • Atualizações do progresso do trabalho

  • Notifications

  • Aplicativos de formulários interativos

Ao criar comunicação com o cliente em seus aplicativos, normalmente há dois componentes:

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

  • Biblioteca do lado do cliente

Os clientes não estão limitados a navegadores – aplicativos móveis, aplicativos de console e outros aplicativos nativos também podem se comunicar usando o SignalR/WebSockets. O programa simples a seguir ecoa todo o conteúdo enviado para um aplicativo de bate-papo 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 as maneiras pelas quais seus aplicativos se comunicam diretamente com os aplicativos cliente e considere se a comunicação em tempo real melhoraria a experiência do usuário do seu aplicativo.

Referências – Comunicação com o Cliente

Design orientado por domínio – Você deve aplicá-lo?

Domain-Driven Design (DDD) é uma abordagem ágil para a construçã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 em domínio de negócios que podem se relacionar com os desenvolvedores como o sistema do mundo real funciona. Por exemplo, se você estiver construindo um sistema que lida com negociações de ações, seu especialista em domínio pode ser um corretor de ações experiente. O DDD é projetado para resolver problemas de negócios grandes e complexos, e muitas vezes não é apropriado para aplicações menores e mais simples, pois o investimento em entender e modelar o domínio não vale a pena.

Ao criar software seguindo uma abordagem DDD, sua equipe (incluindo partes interessadas não técnicas e contribuidores) 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 equivalente de software e quaisquer estruturas que possam existir para persistir o conceito (por exemplo, tabelas de banco de dados). Assim, os conceitos descritos na linguagem ubíqua devem formar a base para o seu modelo de domínio.

Seu modelo de domínio compreende objetos que interagem uns com os outros para representar o comportamento do sistema. Esses objetos podem se enquadrar nas seguintes categorias:

  • Entidades, que representam objetos com um fio de identidade. Normalmente, as entidades são armazenadas em persistência com uma chave pela qual 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 que consiste em uma data de início e de término.

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

Um modelo de domínio DDD deve encapsular o comportamento complexo dentro do modelo. As entidades, em particular, não devem ser meras coleções de propriedades. Quando o modelo de domínio carece de comportamento e apenas representa o estado do sistema, diz-se que é um 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 detalhes de persistência.

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

  • Serviços, para encapsular comportamentos complexos e/ou detalhes de implementação de infraestrutura.

  • Command, para desacoplar, emitir comandos e executar o próprio comando.

  • Especificação, para encapsular detalhes da consulta.

O DDD também recomenda o uso da Arquitetura Limpa discutida anteriormente, permitindo acoplamento, encapsulamento e código soltos que podem ser facilmente verificados usando testes de unidade.

Quando deve aplicar o DDD

O DDD é adequado para grandes aplicações com complexidade comercial significativa (não apenas técnica). O aplicativo deve exigir o conhecimento de especialistas em domínio. Deve haver um comportamento significativo no próprio modelo de domínio, representando regras de negócios e 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 DDD

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

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

Referências – Domain-Driven Design

Implementação

Há algumas etapas envolvidas no processo de implantação do aplicativo ASP.NET Core, independentemente de onde ele será hospedado. O primeiro passo é publicar o aplicativo, o que pode ser feito usando o dotnet publish comando CLI. Esta etapa compilará o aplicativo e colocará todos os arquivos necessários para executar o aplicativo em uma pasta designada. Quando você implanta a partir do Visual Studio, esta etapa é executada para você automaticamente. A pasta de publicação contém arquivos .exe e .dll para o aplicativo e suas dependências. Um aplicativo autônomo também incluirá uma versão do tempo de execução do .NET. ASP.NET aplicativos principais também incluirão arquivos de configuração, ativos de cliente estáticos e exibições MVC.

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

Além de 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 reverso fornecem uma camada de segurança para o aplicativo. O Kestrel também não suporta a hospedagem de vários aplicativos na mesma porta, portanto, técnicas como cabeçalhos de host não podem ser usadas com ele para permitir a hospedagem de vários aplicativos na mesma porta e endereço IP.

Peneireiro-das-torres para 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 é proteger vários aplicativos usando SSL/HTTPS. Nesse caso, apenas o proxy reverso precisaria ter o SSL configurado. A comunicação entre o servidor proxy reverso e o Kestrel pode ocorrer por HTTP, como mostra a 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 seu aplicativo ASP.NET Core em um contêiner do Docker, que pode ser hospedado localmente ou implantado no Azure para hospedagem baseada em nuvem. O contêiner do Docker poderia conter o código do seu aplicativo, em execução no Kestrel, e seria implantado atrás de um servidor proxy reverso, como mostrado acima.

Se você estiver hospedando seu aplicativo no Azure, poderá usar o Gateway de Aplicativo do Microsoft Azure como um dispositivo virtual dedicado para fornecer vários serviços. Além de atuar como um proxy reverso para aplicativos individuais, o Application Gateway também pode oferecer os seguintes recursos:

  • Balanceamento de carga HTTP

  • Descarregamento de SSL (SSL apenas para Internet)

  • SSL de ponta a ponta

  • Roteamento multissite (consolide até 20 sites em um único Application Gateway)

  • Firewall de aplicações Web

  • Suporte a Websocket

  • Diagnósticos avançados

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

Referências – Implantação