Como migrar projetos do ASP.NET MVC para ASP.NET Core

Este é um guia prático para migrar um projeto ASP.NET MVC framework para ASP.NET Core.

Fique por dentro
Este passo a passo foi escrito pelo time da nopCommerce que trabalha com projeto de código aberto e pode ser aplicado a qualquer projeto ASP.NET MVC. Descreve por que você precisa migrar, e por que projetos que não acompanham essas tendências deveriam considerá-las.

Antes de seguir para o passo a passo de como ir para do ASP.NET MVC para ASP.NET Core (usando o nopCommerce como exemplo), apresenta-se uma rápida visão das vantagens desse framework.

Este é um guia prático para migrar um projeto ASP.NET MVC framework para ASP.NET Core. Este passo a passo foi escrito pelo time da nopCommerce que trabalha com projeto de código aberto e pode ser aplicado a qualquer projeto ASP.NET MVC. Descreve por que você precisa migrar, e por que projetos que não acompanham essas tendências deveriam considerá-las.

Antes de seguir para o passo a passo de como ir para do ASP.NET MVC para ASP.NET Core (usando o nopCommerce como exemplo), apresenta-se uma rápida visão das vantagens desse framework.

ASP.NET Core tornou-se muito conhecido e a estrutura desenvolvida possui várias atualizações e melhorias, tornando-a bastante estável, tecnologicamente avançada e resistente a ataques XSRF/CSRF, como mostra a Figura 1.

Figura 1. Estrutura do ASP.NET Core

Cross-platform é uma das características que a distingui, fazendo-o mais e mais popular. De agora em diante sua aplicação web pode rodar em Windows e Unix.

Sua arquitetura é modular, pois o ASP.NET Core vem totalmente na forma de pacotes NuGet. Isto permite a optimização da aplicação, incluindo os pacotes necessários juntamente a aplicação, melhorando a performance da solução e reduzindo o tempo que leva para fazer o upgrade das partes separadas. Esta é a segunda característica importante, que permite ao desenvolvedor integrar novos recursos em suas soluções de maneira mais flexível.

O desempenho é um outro passo para a criação de uma aplicação de alta performance. ASP.NET Core processa 2.300% mais requisições por segundo do que o ASP.NET 4.6, e 800% mais requisições por segundo do que o Node.js, como vemos na Figura 2. Você pode checar esses detalhes de performance e testar você mesmo aqui ou ainda baixando o fonte através do link aqui no post.

Figura 2. Comparação de desempenho

Middleware é um novo pipeline leve e rápido para solicitações na aplicação, onde cada parte processa uma solicitação HTTP e decide retornar o resultado ou passa para a próxima parte do middleware. Esta abordagem possibilita ao desenvolvedor controle total sobre o pipeline HTTP e contribui para o desenvolvimento de módulos simples para a aplicação, o que é importante para um projeto de código aberto crescente.

O ASP.NET Core MVC também fornece características que simplificam o desenvolvimento web. O nopCommerce já utiliza algumas delas, tais como templates Modelo-Exibições-Controles (MVC), sintaxe Razor, modelo de dados e validações.

Entre as novas características estão:

É claro que o ASP.NET Core possui muito mais características, mas destacamos algumas das mais interessantes.

Agora considere alguns pontos para lembrar quando transportar sua aplicação para uma nova estrutura.

Migration

As seguintes descrições contêm uma grande quantidade de links para a documentação oficial do ASP.NET Core, que oferecem informações mais detalhadas sobre os tópicos e guia os desenvolvedores que enfrentam estas tarefas pela primeira vez.

Passo 1. Preparando a ferramenta

A primeira coisa que você precisa é migrar o Visual Studio 2017 para versão 15.3 ou posterior e instalar a última versão do .NET Core SDK.

Antes de transferir a aplicação, indica-se usar o .NET Portability Analyzer. Isto pode ser um bom ponto de partida para entender como a transferência de mão de obra intensiva de uma plataforma para outra pode ser. Mesmo assim, esta ferramenta não cobre todos os problemas, pois este processo tem muitas armadilhas a serem resolvidas à medida que surgem. Somente os passos principais e as soluções usadas no projeto nopCommerce são descritas aqui.

A primeira coisa e mais fácil a fazer é atualizar os links para as bibliotecas usadas no projeto para que ele suporte .NET Standard.

Passo 2. Análise de compatibilidade do pacote NuGet para suportar o padrão .NET

Se você usa pacotes NuGet em seus projetos, verifique se eles são compatíveis com o .NET Core. Uma maneira de se fazer isso é usar a ferramenta NuGetPackageExplorer.

Passo 3. O novo formato do arquivo csproj em .NET Core

Uma nova abordagem para adicionar referências a pacotes de terceiros foi introduzida no .NET Core. Ao adicionar uma nova biblioteca de classes, precisamos abrir o arquivo principal do projeto e substituir seu conteúdo da seguinte maneira apresentada na Listagem 1.

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.6" /> ... </ItemGroup> ... </Project>
Listagem 1. Conteúdo do arquivo principal

As referências às bibliotecas conectadas serão carregadas automaticamente. Para mais informações de como comparar as propriedades do projeto project.json e CSPROJ, leia a documentação oficial aqui e aqui.

Passo 4. Alteração da Namespace

Delete todos os uses de System.Web e troque por Microsoft.AspNetCore.

Passo 5. Configure o arquivo Startup.cs em vez de usar global.asax

O ASP.NET Core tem uma nova maneira de carregar o aplicativo: o ponto de entrada do aplicativo é o Startup e não existe dependência do arquivo Global.asax, pois a inicialização registra os middlewares no aplicativo. No Startup deve-se incluir o método Configure e o middleware necessário deve ser adicionado ao pipeline no Configure.

Problemas a serem resolvidos no Startup.cs:

Resta apenas criar visualizações para todas as ações descritas no controlador, como vemos na Listagem 5.

Figura 3. Estrutura com a pasta Admin
[Area("Admin")] [Route("admin")] public class AdminController : Controller { public IActionResult Index() { return View(); } }
Listagem 5. Criando visualizações

Validação

O IformCollection não deverá ser passado para os controladores, pois neste caso, a validação do servidor ASP.NET está desabilitada. O MVC está suprimindo a validação adicional se o IFormCollection não for nulo. Para resolver esse problema, esta propriedade pode ser adicionada para o modelo, impedindo de passar diretamente para o método controlador. Esta regra trabalha somente se o modelo está disponível, caso contrário, não existirão validações.

As propriedades filhas não são mais validadas automaticamente e devem ser especificadas manualmente.

Passo 6. Migrar manipuladores HTTP e HttpModules para Middleware

Manipuladores e módulos HTTP são de fato muito similares ao conceito de Middleware no ASP.NET Core. Contudo, diferentemente dos módulos, a ordem do middleware é baseada em que são inseridos no pipeline de solicitação. A ordem dos módulos é principalmente baseada nos eventos do ciclo de vida do aplicativo. A ordem do middleware para respostas é oposta à ordem de solicitações, enquanto a ordem dos módulos para solicitações e respostas é a mesma.

Sabendo disso, você pode proceder com as seguintes atualizações:

A autenticação no nopCommerce não usa um sistema de autenticação interno. Para esse propósito é usado o AuthenticationMiddleware, desenvolvido de acordo com a nova estrutura do ASP.NET Core, como mostra a Listagem 6.

public class AuthenticationMiddleware { private readonly RequestDelegate _next; public AuthenticationMiddleware(IAuthenticationSchemeProvider schemes, RequestDelegate next) { Schemes = schemes ?? throw new ArgumentNullException(nameof(schemes)); _next = next ?? throw new ArgumentNullException(nameof(next)); } public IAuthenticationSchemeProvider Schemes { get; set; } public async Task Invoke(HttpContext context) { context.Features.Set<IAuthenticationFeature> (new AuthenticationFeature { OriginalPath = context.Request.Path, OriginalPathBase = context.Request.PathBase }); var handlers = context.RequestServices.GetRequiredService< IAuthenticationHandlerProvider>(); foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) { try { if (await handlers.GetHandlerAsync(context, scheme.Name) is IAuthenticationRequestHandler handler && await handler.HandleRequestAsync()) return; } catch { // ignored } } var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); if (defaultAuthenticate != null) { var result = await context.AuthenticateAsync(defaultAuthenticate.Name); if (result?.Principal != null) { context.User = result.Principal; } } await _next(context); } }
Listagem 6. Uso do AuthenticationMiddleware

O ASP.NET fornece muitos middlewares que você pode usar em seu aplicativo, porém o desenvolvedor pode criar o seu próprio middleware e adicioná-lo no pipeline de solicitações HTTP. Para simplificar o processo, adicionou-se uma interface especial no nopCommerce e agora basta criar uma classe que a implemente, como mostra a Listagem 7.

public interface INopStartup { /// <summary> /// Add and configure any of the middleware /// </summary> /// <param name="services">Collection of service descriptors</param> /// <param name="configuration">Configuration of the application</param> void ConfigureServices(IServiceCollection services, IConfiguration configuration); /// <summary> /// Configure the using of added middleware /// </summary> /// <param name="application">Builder for /// configuring an application's request pipeline</param> void Configure(IApplicationBuilder application); /// <summary> /// Gets order of this startup configuration implementation /// </summary> int Order { get; } }
Listagem 7. Interface INopStartup

Na Listagem 8 você pode adicionar e configurar seu middleware.

/// <summary> /// Represents object for the configuring authentication middleware on application startup /// </summary> public class AuthenticationStartup : INopStartup { /// <summary> /// Add and configure any of the middleware /// </summary> /// <param name="services">Collection of service descriptors</param> /// <param name="configuration">Configuration of the application</param> public void ConfigureServices(IServiceCollection services, IConfiguration configuration) { //add data protection services.AddNopDataProtection(); //add authentication services.AddNopAuthentication(); } /// <summary> /// Configure the using of added middleware /// </summary> /// <param name="application">Builder for configuring an /// application's request pipeline</param> public void Configure(IApplicationBuilder application) { //configure authentication application.UseNopAuthentication(); } /// <summary> /// Gets order of this startup configuration implementation /// </summary> public int Order => 500; //authentication should be loaded before MVC }
Listagem 8. Configurando middleware

Passo 7. Usando DI incorporado

Injeção de dependência é uma das características chaves ao projetar um aplicativo ASP.NET Core. Você pode desenvolver aplicativos fracamente acoplados e mais testáveis, modulares e como resultado mais flexíveis na manuseabilidade: isso foi possível seguindo o princípio da inversão de dependência. Para injetar a dependência usamos contêineres IoC (inversão de controle) e em ASP.NET Core esses contêineres são representados pela interface IServiceProvider. Os serviços são instalados no aplicativo no método Startup.ConfigureServices(), como vemos na Listagem 9.

Qualquer serviço registrado pode ser configurado com três escopos:

services.AddDbContext<ApplicationDbContext> (options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddSingleton<Isingleton,MySingleton>();
Listagem 9. Escopo do serviço

Passo 8. Usando projetos WebAPI compatíveis com shells (Shim)

Para simplificar a migração de um WebAPI existente, faça o uso do pacote NuGet (Listagem 10) Microsoft.AspNetCore.Mvc.WebApiCompatShim, pois ele suporta os seguintes recursos compatíveis:

services.AddMvc().AddWebApiConventions(); routes.MapWebApiRoute(name: "DefaultApi", template: "api//{id?}" );
Listagem 10. Uso do pacote NuGet

Passo 9. Conversão das Configurações da Aplicação

Algumas configurações foram salvas anteriormente no arquivo web.config. Agora existe uma nova abordagem, baseada nos pares de chave-valores definidos pelos provedores de configuração. Este é o método recomendado pelo ASP.NET Core, e o NopCommerce usa o arquivo appsettings.json.

Você também pode usar o pacote NuGet System.Configuration.ConfigurationManager, mas se por alguma razão quiser continuar usando o *.config a aplicação não pode rodar na plataforma Unix, mas somente no IIS.

Se você quer usar o provedor de configurações de armazenamento de chaves do Azure, então precisará referenciar a migração de conteúdo para o Azure de chaves-valores, mas o projeto não contém essa tarefa.

Passo 10. Conversão conteúdo estático para wwwroot

Para servir conteúdo estático, especifique para o web host a raiz (root) como sendo o diretório corrente, conforme Figura 4. O default é wwwroot. Você pode configurar sua pasta para armazenamento de arquivos estáticos através da configuração do middleware.

Figura 4. Pasta raiz

Passo 11. Conversão do Entity Framework para EF Core

Se o projeto usa algumas características do Entity Framework 6 que não são suportadas no EF Core, faz sentido rodar a aplicação no .NET Framework. Nesse caso, rejeitaremos as características multiplataforma e o aplicativo será executado apenas no IIS.

A seguir estão as principais alterações a serem consideradas e apresentadas na Listagem 11:

/// <summary> /// Register base object context /// </summary> /// <param name="services">Collection of service descriptors</param> public static void AddNopObjectContext(this IServiceCollection services) { services.AddDbContextPool<NopObjectContext>(optionsBuilder => { optionsBuilder.UseSqlServerWithLazyLoading(services); }); } /// <summary> /// SQL Server specific extension method for /// Microsoft.EntityFrameworkCore.DbContextOptionsBuilder /// </summary> /// <param name="optionsBuilder">Database context options builder</param> /// <param name="services">Collection of service descriptors</param> public static void UseSqlServerWithLazyLoading(this DbContextOptionsBuilder optionsBuilder, IServiceCollection services) { var nopConfig = services.BuildServiceProvider().GetRequiredService<NopConfig>(); var dataSettings = DataSettingsManager.LoadSettings(); if (!dataSettings?.IsValid ?? true) return; var dbContextOptionsBuilder = optionsBuilder.UseLazyLoadingProxies(); if (nopConfig.UseRowNumberForPaging) dbContextOptionsBuilder.UseSqlServer(dataSettings.DataConnectionString, option => option.UseRowNumberForPaging()); else dbContextOptionsBuilder.UseSqlServer(dataSettings.DataConnectionString); }
Listagem 11. Conversão do Entity Framework para EF Core

Para verificar se o EF Core gera uma estrutura de banco de dados semelhante ao Entity Framework quando migrar, utilize a ferramenta SQL Compare.

Passo 12. Removendo todas as referências HttpContext, substituindo classes obsoletas e alterando o namespace

Durante o projeto de migração você encontrará muitas classes que deverão ser renomeadas ou removidas e agora é necessário cumprir com os novos requisitos. Por isso, aqui está uma lista das principais alterações:

Trocas no namespace:

Outros:

Passo 13. Atualização de autenticação e autorização

Como já foi mencionado acima, o projeto nopCommerce não tem o sistema de autenticação embutido, pois é implementado em uma camada de middleware separado. No entanto, o ASP.NET Core possui seu próprio sistema para fornecer credenciais. Você pode ver a documentação para saber mais detalhes.

Em relação a proteção de dados, o nopCommerce não usa mais MachineKey. Em vez disso, usamos o recurso interno de proteção de dados. Por padrão, as chaves são geradas quando o aplicativo é iniciado. O armazenamento de dados pode ser:

Caso os provedores internos não sejam adequados pode-se especificar seu próprio provedor de armazenamento de chaves criando um IXmlRepository.

Passo 14. Atualização JS/CSS

A maneira de usar os recursos estáticos mudou e agora todos eles devem ser armazenados na pasta da raiz do projeto wwwroot, a menos que outras configurações sejam feitas.

Ao usar blocos internos de JavaScript, recomendamos movê-los para o final da página. Apenas use o atributo asp-location = "Footer" para suas tags <script>. As mesmas regras se aplicam aos arquivos .js.

Use a extensão BundlerMinifier como substituto para a System.Web.Optimization, pois isso permitirá o empacotamento e a minificação do JavaScript e CSS durante a criação do projeto (conforme a documentação).

Passo 15. Conversão de exibições

Primeiro, Child Actions não são mais usadas, já que o ASP.NET Core sugere o uso de uma nova ferramenta de alto desempenho. Os ViewComponents são chamados de forma assíncrona.

Veja na Listagem 12 como obter uma string da ViewComponent.

/// <summary> /// Render component to string /// </summary> /// <param name="componentName">Component name</param> /// <param name="arguments">Arguments</param> /// <returns>Result</returns> protected virtual string RenderViewComponentToString (string componentName, object arguments = null) { if (string.IsNullOrEmpty(componentName)) throw new ArgumentNullException(nameof(componentName)); var actionContextAccessor = HttpContext.RequestServices.GetService(typeof (IActionContextAccessor)) as IActionContextAccessor; if (actionContextAccessor == null) throw new Exception("IActionContextAccessor cannot be resolved"); var context = actionContextAccessor.ActionContext; var viewComponentResult = ViewComponent(componentName, arguments); var viewData = ViewData; if (viewData == null) { throw new NotImplementedException(); } var tempData = TempData; if (tempData == null) { throw new NotImplementedException(); } using (var writer = new StringWriter()) { var viewContext = new ViewContext( context, NullView.Instance, viewData, tempData, writer, new HtmlHelperOptions()); // IViewComponentHelper is stateful, we want to make sure to // retrieve it every time we need it. var viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService< IViewComponentHelper>(); (viewComponentHelper as IViewContextAware)?.Contextualize(viewContext); var result = viewComponentResult.ViewComponentType == null ? viewComponentHelper.InvokeAsync(viewComponentResult.ViewComponentName, viewComponentResult.Arguments): viewComponentHelper.InvokeAsync(viewComponentResult.ViewComponentType, viewComponentResult.Arguments); result.Result.WriteTo(writer, HtmlEncoder.Default); return writer.ToString(); } }
Listagem 12. Obtendo uma string com a ViewComponent

Perceba que não é mais necessário usar o HtmlHelper, pois o ASP.NET Core inclui muito auxiliares que são as auxiliares de Tag. Quando o aplicativo está em execução, o mecanismo Razor as processa no servidor e converte-as em elementos HTML padrão. Isso torna o desenvolvimento de aplicativos muito mais fácil, mas é claro que você pode implementar seus próprios auxiliares de tags.

Inicializamos o uso de injeção de dependência nas visualizações ao invés de habilitar configurações e serviços usando o EngineContext. Com isso, os principais pontos a serem considerados na conversão são:

Conclusão

O processo de migração de um grande aplicativo Web é uma tarefa muito demorada que, como regra, não pode ser realizada sem as “armadilhas”. Podemos planejar migrar para uma nova estrutura, mas quando a sua primeira versão estável ficou pronta não foi possível lançá-la imediatamente pois haviam alguns recursos críticos que não tinham sido transferidos para o .NET Core. Em particular, os relacionados ao Entity Framework.

Portanto, foi necessário fazer o lançamento com uma abordagem mista, com a arquitetura do .NET Core e as dependências do .NET Framework, que por si só formam uma solução exclusiva. Ser o primeiro não é fácil e a certeza de que foi a escolha certa, com o apoio da nossa enorme comunidade.

O projeto foi totalmente adaptado após o lançamento do .NET Core 2.1, tendo nessa época uma solução estável já trabalhando na nova arquitetura. Restou apenas substituir alguns pacotes e reescrever o trabalho com o EF Core. Assim, transcorreram vários meses e duas versões lançadas para migrar completamente para a nova estrutura.

Pode-se dizer com confiança que este é o primeiro grande projeto a realizar essa migração. Neste guia foi mostrado todo o processo de migração de forma estruturada e descrevemos vários gargalos para que outros desenvolvedores possam confiar nesse material e seguir o roteiro ao resolver a mesma tarefa.

Artigos relacionados