Aquí hay una guía práctica sobre cómo migrar un proyecto del marco ASP.NET MVC a ASP.NET Core. Las instrucciones paso a paso escritas por el equipo del proyecto de código abierto nopCommerce se pueden aplicar fácilmente a cualquier proyecto ASP.NET MVC.
También describe por qué es posible que deba actualizar y por qué los proyectos que aún no siguen el ritmo deberían considerarlo.
Antes de continuar con los pasos para migrar de ASP.NET MVC a ASP.NET Core (usando nopCommerce como ejemplo), veamos una descripción general rápida de las ventajas de este marco.
ASP.NET Core ya es un marco bastante conocido y desarrollado con varias actualizaciones importantes que lo hacen bastante estable, tecnológicamente avanzado y resistente a los ataques XSRF/CSRF.
La multiplataforma es una de las características distintivas que la hacen cada vez más popular. A partir de ahora, su aplicación web puede ejecutarse tanto en entornos Windows como Unix.
Arquitectura modular: ASP.NET Core viene completamente en forma de paquetes NuGet, permite optimizar la aplicación, incluidos los paquetes necesarios seleccionados. Esto mejora el rendimiento de la solución y reduce el tiempo que lleva actualizar partes separadas.
Esta es la segunda característica importante que permite a los desarrolladores integrar nuevas funciones en su solución de manera más flexible.
El rendimiento es otro paso hacia la creación de una aplicación de alto rendimiento. ASP.NET Core maneja un 2.300 % más de solicitudes por segundo que ASP.NET 4.6 y un 800 % más de solicitudes por segundo que node.js.
Puede consultar las pruebas de rendimiento detalladas usted mismo aquí o aquí .
Las mejores respuestas de texto sin formato por segundo, entorno de prueba
Middleware es una nueva canalización modular ligera y rápida para solicitudes dentro de la aplicación. Cada parte del middleware procesa una solicitud HTTP y luego decide devolver el resultado o pasa la siguiente parte del middleware.
Este enfoque brinda al desarrollador control total sobre la canalización HTTP y contribuye al desarrollo de módulos simples para la aplicación, lo cual es importante para un proyecto de código abierto en crecimiento.
Además, ASP.NET Core MVC proporciona funciones que simplifican el desarrollo web. nopCommerce ya usó algunos de ellos, como la plantilla Model-View-Controller, la sintaxis de Razor, el enlace de modelos y la validación. Entre las nuevas características se encuentran:
Por supuesto, ASP.NET Core tiene muchas más funciones, solo vimos las más interesantes.
Ahora, consideremos los puntos a tener en cuenta al migrar su aplicación a un nuevo marco.
Las siguientes descripciones contienen una gran cantidad de enlaces a la documentación oficial de ASP.NET Core para brindar información más detallada sobre el tema y guiar a los desarrolladores que se enfrentan a esta tarea por primera vez.
Paso 1. Preparar un conjunto de herramientas
Lo primero que necesita es actualizar Visual Studio 2017 a la versión 15.3 o posterior e instalar la última versión de .NET Core SDK.
Antes de portar, recomendamos utilizar .Net Portability Analyzer . Este puede ser un buen punto de partida para comprender cuán laboriosa será la migración de una plataforma a otra. Sin embargo, esta herramienta no cubre todos los problemas, este proceso tiene muchos escollos que deben resolverse a medida que surgen.
A continuación, describiremos los pasos principales y las soluciones utilizadas en el proyecto nopCommerce.
Lo primero y más fácil de hacer es actualizar los enlaces a las bibliotecas utilizadas en el proyecto para que sean compatibles con .NET Standard.
Paso 2. Análisis de compatibilidad de paquetes NuGet para admitir el estándar .Net
Si usa paquetes NuGet en su proyecto, verifique si son compatibles con .NET Core. Una forma de hacerlo es usar la herramienta NuGetPackageExplorer .
Paso 3. El nuevo formato de archivo csproj en .NET Core
Se introdujo un nuevo enfoque para agregar referencias a paquetes de terceros en .NET Core. Al agregar una nueva biblioteca de clases, debe abrir el archivo del proyecto principal y reemplazar su contenido de la siguiente manera:
< Project Sdk = "Microsoft.NET.Sdk" > < PropertyGroup > < TargetFramework > netcoreapp2.2 </ TargetFramework > </ PropertyGroup > < ItemGroup > < PackageReference Include = "Microsoft.AspNetCore.App" Version = "2.2.6" /> ... </ ItemGroup > ... </ Project >
Las referencias a las bibliotecas conectadas se cargarán automáticamente.
Para obtener más información sobre cómo comparar las propiedades de project.json y CSPROJ, lea la documentación oficial aquí y aquí.
Paso 4. Actualización del espacio de nombres
Elimine todos los usos de System.Web y reemplácelos con Microsoft.AspNetCore.
Paso 5. Configure el archivo Startup.cs en lugar de usar global.asax
ASP.NET Core tiene una nueva forma de cargar la aplicación. El punto de entrada de la aplicación es
Startup
, y no hay dependencia en el archivo Global.asax. Startup
registra los middlewares en la aplicación. Startup
debe incluir la Configure
método. El middleware requerido debe agregarse a la canalización en Configure
.Problemas a resolver en Startup.cs:
//add basic MVC feature var mvcBuilder = services.AddMvc(); //add custom model binder provider (to the top of the provider list) mvcBuilder.AddMvcOptions(options => options.ModelBinderProviders.Insert( 0 , new NopModelBinderProvider()));
/// <summary> /// Represents model binder provider for the creating NopModelBinder /// </summary> public class NopModelBinderProvider : IModelBinderProvider { /// <summary> /// Creates a nop model binder based on passed context /// </summary> /// <param name="context"> Model binder provider context </param> /// <returns> Model binder </returns> public IModelBinder GetBinder ( ModelBinderProviderContext context ) { if (context == null ) throw new ArgumentNullException( nameof (context)); var modelType = context.Metadata.ModelType; if (! typeof (BaseNopModel).IsAssignableFrom(modelType)) return null ; //use NopModelBinder as a ComplexTypeModelBinder for BaseNopModel if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType) { //create binders for all model properties var propertyBinders = context.Metadata.Properties .ToDictionary(modelProperty => modelProperty, modelProperty => context.CreateBinder(modelProperty)); return new NopModelBinder(propertyBinders, EngineContext.Current.Resolve<ILoggerFactory>()); } //or return null to further search for a suitable binder return null ; } }
app.UseMvc(routes => { routes.MapRoute( "areaRoute" , "{area:exists}/{controller=Admin}/{action=Index}/{id?}" ); routes.MapRoute( name: "default" , template: "{controller=Home}/{action=Index}/{id?}" ); });
Al hacerlo, la carpeta con el nombre Area con la carpeta Admin dentro, debe estar en la raíz de la aplicación. Ahora el atributo
[Area("Admin")] [Route("admin")].
se utilizará para conectar el controlador con esta área.Solo queda crear vistas para todas las acciones descritas en el controlador.
[ Area( "Admin" ) ] [ Route( "admin" ) ] public class AdminController : Controller { public IActionResult Index ( ) { return View(); } }
Validación
IFormCollection no debe pasarse a los controladores ya que, en este caso, la validación del servidor asp.net está deshabilitada: MVC está suprimiendo la validación adicional si se encuentra que IFormCollection no es nulo. Para resolver el problema, esta propiedad podría agregarse al modelo, esto evitará que pasemos directamente al método del controlador.
Esta regla funciona solo si hay un modelo disponible, si no hay modelo, no habrá validación.
Las propiedades secundarias ya no se validan automáticamente y deben especificarse manualmente.
Paso 6. Migrar controladores HTTP y HttpModules a Middleware
Los controladores HTTP y los módulos HTTP son, de hecho, muy similares al concepto de Middleware en ASP.NET Core , pero a diferencia de los módulos, el orden del middleware se basa en el orden en que se insertan en la canalización de solicitudes. El orden de los módulos se basa principalmente en los eventos del ciclo de vida de la aplicación. El orden del middleware para las respuestas es opuesto al orden de las solicitudes, mientras que el orden de los módulos para las solicitudes y las respuestas es el mismo. Sabiendo esto, puede continuar con la actualización.
Lo que debe actualizarse:
La autenticación en nopCommerce no utiliza un sistema de autenticación integrado; para ello se utiliza AuthenticationMiddleware desarrollado de acuerdo con la nueva estructura ASP.NET Core.
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); } }
ASP.NET proporciona una gran cantidad de middleware integrado que puede usar en su aplicación, pero el desarrollador también puede crear su propio middleware y agregarlo a la canalización de solicitudes HTTP.
Para simplificar este proceso, agregamos una interfaz especial a nopCommerce, y ahora basta con crear una clase que la implemente.
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 ; } }
Aquí puede agregar y configurar su 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 }
Paso 7. Usando DI incorporado
La inyección de dependencia es una de las características clave al diseñar una aplicación en ASP.NET Core. Puede desarrollar aplicaciones débilmente acopladas que sean más comprobables, modulares y, como resultado, más fáciles de mantener. Esto fue posible siguiendo el principio de inversión de dependencia.
Para inyectar dependencias, usamos contenedores IoC (Inversion of Control). En ASP.NET Core, dicho contenedor está representado por la interfaz IServiceProvider. Los servicios se instalan en la aplicación en el método Startup.ConfigureServices().
Cualquier servicio registrado se puede configurar con tres alcances:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString( "DefaultConnection" ))); services.AddSingleton<Isingleton,MySingleton>();
Paso 8. Uso de shells de compatibilidad de proyectos WebAPI (Shim)
Para simplificar la migración de una API web existente, recomendamos usar el paquete NuGet Microsoft.AspNetCore.Mvc.WebApiCompatShim . Es compatible con las siguientes funciones compatibles :
services.AddMvc().AddWebApiConventions(); routes.MapWebApiRoute(name: "DefaultApi" , template: "api/{controller}/{id?}" );
Paso 9. Configuración de la aplicación de portabilidad
Algunas configuraciones se guardaron previamente en el archivo web.config. Ahora usamos un nuevo enfoque basado en los pares clave-valor establecidos por los proveedores de configuración . Este es el método recomendado en ASP.NET Core y usamos el archivo appsettings.json.
También puede usar el paquete NuGet
System.Configuration.ConfigurationManager
si por alguna razón desea continuar usando *.config. En este caso, la aplicación no puede ejecutarse en plataformas Unix, sino solo en IIS.Si desea usar el proveedor de configuración de almacenamiento de claves de Azure, debe consultar Migración de contenido a Azure Key Vault . Nuestro proyecto no contenía tal tarea.
Paso 10. Portar contenido estático a wwwroot
Para servir contenido estático , especifique para hospedar web la raíz de contenido del directorio actual. El valor predeterminado es wwwroot. Puede configurar su carpeta para almacenar archivos estáticos configurando un middleware.
Paso 11. Portar EntityFramework a EF Core
Si el proyecto usa algunas características específicas de Entity Framework 6 , que no son compatibles con EF Core, tiene sentido ejecutar la aplicación en NET Framework. Aunque en este caso, tendremos que rechazar la función multiplataforma y la aplicación se ejecutará solo en Windows e IIS.
A continuación se detallan los principales cambios a tener en cuenta:
/// <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); }
Para verificar que EF Core genera una estructura de base de datos similar a la de Entity Framework al migrar , use la herramienta SQL Compare .
Paso 12. Eliminar todas las referencias de HttpContext y reemplazar las clases obsoletas y cambiar el espacio de nombres
Durante la migración del proyecto, encontrará que se ha cambiado el nombre o movido una cantidad suficientemente grande de clases, y ahora debe cumplir con los nuevos requisitos. Aquí hay una lista de los principales cambios que puede encontrar:
Reemplazo del espacio de nombres:
Otro:
Paso 13. Actualización de autenticación y autorización
Como ya se mencionó anteriormente, el proyecto nopCommerce no involucra el sistema de autenticación incorporado, se implementa en una capa de middleware separada.
Sin embargo, ASP.NET Core tiene su propio sistema para proporcionar credenciales. Puede ver la documentación para conocerlos en detalle.
En cuanto a la protección de datos, ya no usamos MachineKey . En su lugar, utilizamos la función de protección de datos integrada. De forma predeterminada, las claves se generan cuando se inicia la aplicación. Como el almacenamiento de datos puede ser:
Si los proveedores integrados no son adecuados, puede especificar su propio proveedor de almacenamiento de claves creando un IXmlRepository personalizado.
Paso 14. Actualización de JS/CSS
La forma de usar los recursos estáticos ha cambiado, ahora todos deben almacenarse en la carpeta raíz del proyecto wwwroot , a menos que se realicen otras configuraciones.
Cuando utilice bloques integrados de javascript, le recomendamos moverlos al final de la página. Simplemente use el atributo asp-ubicación = "Pie de página" para sus etiquetas <script>. La misma regla se aplica a los archivos js.
Use la extensión BundlerMinifier como reemplazo de System.Web.Optimization; esto permitirá la agrupación y la minificación. JavaScript y CSS durante la construcción del proyecto ( ver la documentación ).
Paso 15. Portabilidad de vistas
En primer lugar, las acciones secundarias ya no se usan; en su lugar, ASP.NET Core sugiere usar una nueva herramienta de alto rendimiento: ViewComponents llamados de forma asíncrona.
Cómo obtener una cadena de 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(); } }
Tenga en cuenta que ya no es necesario usar HtmlHelper, ASP.NET Core incluye muchos asistentes auxiliares de etiquetas incorporados. Cuando la aplicación se está ejecutando, el motor de Razor los maneja en el servidor y, en última instancia, los convierte en elementos html estándar.
Esto hace que el desarrollo de aplicaciones sea mucho más fácil. Y, por supuesto, puede implementar sus propios asistentes de etiquetas.
Comenzamos a usar la inserción de dependencias en las vistas en lugar de habilitar la configuración y los servicios mediante EngineContext.
Por lo tanto, los puntos principales sobre la portabilidad de vistas son los siguientes:
El proceso de migración de una aplicación web grande es una tarea que requiere mucho tiempo y que, por regla general, no puede llevarse a cabo sin las trampas. Planeamos migrar a un nuevo marco tan pronto como se lanzó su primera versión estable, pero no pudimos hacerlo de inmediato, ya que había algunas funciones críticas que no se habían transferido a .NET Core, en particular, aquellas relacionadas con EntityFramework. .
Por lo tanto, primero tuvimos que hacer nuestro lanzamiento utilizando un enfoque mixto: la arquitectura .NET Core con las dependencias de .NET Framework, que en sí misma es una solución única.
Ser el primero no es fácil, pero estamos seguros de haber tomado la decisión correcta y nuestra gran comunidad nos apoyó en esto.
Pudimos adaptar completamente nuestro proyecto después del lanzamiento de .NET Core 2.1, y en ese momento teníamos una solución estable que ya funcionaba en la nueva arquitectura. Solo quedaba reemplazar algunos paquetes y reescribir el trabajo con EF Core.
Por lo tanto, nos tomó varios meses y dos versiones publicadas para migrar completamente al nuevo marco.
Podemos decir con confianza que somos el primer gran proyecto para llevar a cabo tal migración. En esta guía, intentamos reunir todo el proceso de migración de forma estructurada y describir varios cuellos de botella para que otros desarrolladores pudieran confiar en este material y seguir la hoja de ruta al resolver la misma tarea.