天天看点

从ASP.NET MVC迁移到ASP.NET Core的分步指南

分步指南

这是有关将项目从ASP.NET MVC框架迁移到ASP.NET Core的实用指南。 由nopCommerce开源项目团队编写的分步说明可以轻松地应用于任何ASP.NET MVC项目。

它还描述了为什么您可能需要升级,以及为什么尚未跟上进度的项目应该考虑它。

为什么要移植到ASP.NET Core?

在继续进行从ASP.NET MVC移植到ASP.NET Core的步骤(以nopCommerce为例)之前,让我们快速概述一下此框架的优点。

ASP.NET Core已经是一个相当著名的开发框架,并进行了几次重大更新,使其相当稳定,技术先进并且可以抵抗XSRF / CSRF攻击。

从ASP.NET MVC迁移到ASP.NET Core的分步指南

跨平台是使其越来越受欢迎的显着特征之一。 从现在开始,您的Web应用程序可以在Windows和Unix环境中运行。

模块化体系结构-ASP.NET Core完全以NuGet软件包的形式提供,它允许优化应用程序,包括选定的必需软件包。 这样可以提高解决方案性能,并减少升级各个零件所需的时间。

这是第二个重要特征,它使开发人员可以更灵活地将新功能集成到其解决方案中。

性能是构建高性能应用程序的又一步。 与ASP.NET 4.6相比,ASP.NET Core每秒处理的请求数量增加了2.300%,比node.js每秒处理的请求数量增加了800%。

您可以在此处或此处自行检查详细的性能测试。

从ASP.NET MVC迁移到ASP.NET Core的分步指南

每秒最佳的纯文本响应,测试环境

中间件是针对应用程序内请求的新型轻量级快速模块化管道。 中间件的每个部分处理一个HTTP请求,然后决定返回结果或传递中间件的下一部分。

这种方法使开发人员可以完全控制HTTP管道,并有助于开发应用程序的简单模块,这对于不断发展的开源项目很重要。

同样,ASP.NET Core MVC提供了简化Web开发的功能。 nopCommerce已经使用了其中的一些,例如Model-View-Controller模板,Razor语法,模型绑定和验证。 这些新功能包括:

  • 标记助手。 服务器部分代码,用于参与在Razor文件中创建和呈现HTML元素。
  • 查看组件。 一种新工具,类似于局部视图,但性能更高。 当需要重用渲染逻辑并且任务对于部分视图而言过于复杂时,nopCommerce将使用视图组件。
  • DI在视图中。 尽管视图中显示的大多数数据都来自控制器,但nopCommerce也具有视图,在这些视图中依赖注入更为方便。

当然,ASP.NET Core具有更多功能,我们仅查看了最有趣的功能。

现在,让我们考虑将应用程序移植到新框架时要牢记的几点。

移民

以下描述包含指向官方ASP.NET Core文档的大量链接,以提供有关该主题的更多详细信息,并指导首次遇到此类任务的开发人员。

步骤1.准备工具箱

您需要做的第一件事是将Visual Studio 2017升级到15.3版或更高版本,并安装最新版本的.NET Core SDK。

移植之前,建议使用.Net Portability Analyzer 。 这可能是了解从一个平台到另一个平台的劳动密集型移植的一个很好的起点。 但是,此工具不能涵盖所有问题,此过程有很多陷阱需要解决。

下面我们将描述nopCommerce项目中使用的主要步骤和解决方案。

首先也是最简单的事情是更新到项目中使用的库的链接,以便它们支持.NET Standard。

步骤2. NuGet软件包兼容性分析以支持.Net标准

如果在项目中使用NuGet软件包,请检查它们是否与.NET Core兼容。 一种方法是使用NuGetPackageExplorer工具。

第3步。.NETCore中的csproj文件的新格式

.NET Core中引入了一种用于添加对第三方程序包的引用的新方法。 添加新的类库时,需要打开主项目文件并按如下所示替换其内容:

< Project Sdk = "Microsoft.NET.Sdk" >
  < PropertyGroup >
    < TargetFramework > netcoreapp2.2 </ TargetFramework >   
  </ PropertyGroup >
  < ItemGroup >
    < PackageReference Include = "Microsoft.AspNetCore.App" Version = "2.2.6" />
    ...
  </ ItemGroup >
  ...
</ Project >
           

对连接的库的引用将自动加载。

有关比较project.json和CSPROJ属性的更多信息,请在 此处 和 此处 阅读官方文档 。

步骤4.命名空间更新

删除所有对System.Web的使用,并将其替换为Microsoft.AspNetCore。

步骤5.配置Startup.cs文件而不是使用global.asax

ASP.NET Core具有一种加载应用程序的新方法。 应用程序的入口点是

Startup

,并且对Global.asax文件没有依赖性。

Startup

在应用程序中注册中间件。

Startup

必须包括

Configure

方法。 所需的中间件应添加到

Configure

的管道中。

在Startup.cs中要解决的问题:

  1. 为MVC和WebAPI请求配置中间件
  2. 配置为:
  • 异常处理 。 在移植过程中,您将不可避免地面临各种冲突,从而可以在开发环境中做好准备并设置异常处理。 通过UseDeveloperExceptionPage ,我们添加了中间件来捕获异常。
  • MVC路由 。 新路线的注册也已更改。 现在使用IRouteBuilder代替RouteCollection,作为注册限制的新方法(IActionConstraint)
  • MVC / WebAPI筛选器。 筛选器应根据ASP.NET Core的新实现进行更新。
  • MVC / WebAPI格式化程序
  • 绑定模型
  • //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 ;
        }
    }
               
  • 地区 。 要将Area包含在ASP.NET Core应用程序中,请向Startup.cs文件添加常规路由。 例如,以这种方式它将寻找配置Admin /区域
  • app.UseMvc(routes => { routes.MapRoute( "areaRoute" , "{area:exists}/{controller=Admin}/{action=Index}/{id?}" ); routes.MapRoute( name: "default" , template: "{controller=Home}/{action=Index}/{id?}" ); });
               
    这样做时,名称为Area且内部带有Admin文件夹的文件夹应位于应用程序根目录中。 现在,属性

    [Area("Admin")] [Route("admin")].

    将用于将控制器与此区域连接。

    仅保留为控制器中描述的所有操作创建视图。

    从ASP.NET MVC迁移到ASP.NET Core的分步指南
    [ Area( "Admin" ) ]
    [ Route( "admin" ) ]
    public class AdminController : Controller
    {    
        public IActionResult Index ( )
        {
            return View();
        }    
    }
               

    验证方式

    不应该将IFormCollection传递给控制器​​,因为在这种情况下,将禁用asp.net服务器验证-如果发现IFormCollection不为null,则MVC将禁止进一步的验证。 为了解决该问题,可以将此属性添加到模型中,这将阻止我们直接传递给控制器​​方法。

    仅当模型可用时,此规则才有效;如果没有模型,则不会进行验证。

    子属性不再自动验证,应手动指定。

    步骤6.将HTTP处理程序和HttpModules迁移到中间件

    HTTP处理程序和HTTP模块实际上与ASP.NET Core中的中间件概念非常相似,但是与模块不同,中间件顺序是基于将它们插入请求管道的顺序。 模块的顺序主要基于应用程序生命周期的事件。 响应的中间件顺序与请求的顺序相反,而请求和响应的模块顺序相同。 知道这一点,您可以继续进行更新。

    应该更新什么:

    • 中间件模块的迁移(AuthenticationMiddleware,CultureMiddleware等)
    • 中间件处理程序
    • 使用新的中间件
    nopCommerce中的身份验证不使用内置的身份验证系统。 为此,使用了根据新的ASP.NET Core结构开发的AuthenticationMiddleware。
    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提供了许多内置的中间件,您可以在应用程序中使用它们,但是开发人员还可以创建自己的中间件并将其添加到HTTP请求管道中。

    为了简化此过程,我们在nopCommerce中添加了一个特殊的接口,现在只需创建一个实现该接口的类就足够了。

    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 ; }
    }
               
    您可以在此处添加和配置中间件:
    /// <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
    }
               

    步骤7.使用内置DI

    在ASP.NET Core中设计应用程序时, 依赖注入是关键功能之一。 您可以开发松散耦合的应用程序,这些应用程序更易于测试,模块化并且因此更易于维护。 通过遵循依赖倒置的原理,可以做到这一点。

    为了注入依赖关系,我们使用了IoC(控制反转)容器。 在ASP.NET Core中,这样的容器由IServiceProvider接口表示。 服务通过Startup.ConfigureServices()方法安装在应用程序中。

    任何注册的服务都可以配置三个范围:

    • 短暂的
    • 范围
    • 单身人士
    services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString( "DefaultConnection" )));
    services.AddSingleton<Isingleton,MySingleton>();
               

    步骤8.使用WebAPI项目兼容性外壳程序(Shim)

    为了简化现有Web API的迁移,建议使用NuGet包Microsoft.AspNetCore.Mvc.WebApiCompatShim 。 它支持以下兼容功能 :

    • 添加ApiController类型;
    • 启用Web API样式模型绑定;
    • 扩展模型绑定,以便控制器操作可以接受HttpRequestMessage类型的参数;
    • 添加消息格式化程序,以使操作能够返回HttpResponseMessage类型的结果。
    services.AddMvc().AddWebApiConventions();
    
    routes.MapWebApiRoute(name: "DefaultApi" ,
      	template: "api/{controller}/{id?}"
    );
               

    步骤9.移植应用程序配置

    某些设置先前已保存在web.config文件中。 现在,我们基于配置提供程序设置的键值对使用一种新方法 。 这是ASP.NET Core中推荐的方法,我们使用appsettings.json文件。

    如果出于某些原因要继续使用* .config,也可以使用NuGet包

    System.Configuration.ConfigurationManager

    。 在这种情况下,该应用程序不能在Unix平台上运行,而只能在IIS上运行。

    如果要使用Azure密钥存储配置提供程序,则需要参考将内容迁移到Azure密钥保险库 。 我们的项目不包含此类任务。

    步骤10.将静态内容移植到wwwroot

    要提供静态内容 ,请指定将当前目录的内容根目录进行网络托管。 默认值为wwwroot。 您可以通过设置中间件来配置用于存储静态文件的文件夹。

    从ASP.NET MVC迁移到ASP.NET Core的分步指南

    步骤11.将EntityFramework移植到EF Core

    如果项目使用Entity Framework 6的某些特定功能 ,而EF Core 不支持这些功能 ,则在NET Framework上运行应用程序是有意义的。 尽管在这种情况下,我们将不得不拒绝多平台功能,并且该应用程序只能在Windows和IIS上运行。

    以下是要考虑的主要更改:

    • System.Data.Entity命名空间由Microsoft.EntityFrameworkCore替换;
    • DbContext构造函数的签名已更改。 现在您应该注入DbContextOptions;
    • HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)方法替换为ValueGeneratedNever();
    • WillCascadeOnDelete(false)方法被OnDelete(DeleteBehavior.Restrict)取代;
    • OnModelCreating(DbModelBuilder modelBuilder)方法替换为OnModelCreating(ModelBuilder modelBuilder);
    • HasOptional方法不再可用;
    • 由于EntityTypeConfiguration不再可用,因此更改了对象配置,现在正在使用OnModelCreating。
    • ComplexType属性不再可用;
    • IDbSet接口被DbSet取代;
    • ComplexType-复杂类型支持出现在EF Core 2中,具有“ 拥有的实体”类型,并且在EF Core 2.1中没有带有QueryType的主键的表;
    • 与使用[Entity] _Id模板的EF6不同,EF Core中的外键使用[Entity] Id模板生成阴影属性 。 因此,首先将外部键作为常规属性添加到实体;
    • 要为DbContext支持DI,请在ConfigureServices中配置DbContex。
    /// <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);
    }
               

    要验证EF Core在迁移时是否生成与Entity Framework类似的数据库结构,请使用SQL比较工具。

    步骤12.删除所有HttpContext引用并替换过时的类并更改名称空间

    在项目迁移期间,您会发现已重命名或移动了足够多的类,现在您应该遵守新的要求。 以下是您可能会遇到的主要变化的列表:

    • HttpPostedFileBase🡪FormFile
    • 现在可以通过IHttpContextAccessor访问Access HttpContext
    • HtmlHelper🡪HtmlHelper
    • ActionResult🡪ActionResult
    • HttpUtility🡪WebUtility
    • 可从HttpContext.Session访问ISession而不是HttpSessionStateBase。 来自Microsoft.AspNetCore.Http
    • Request.Cookies返回IRequestCookieCollection:IEnumerable <KeyValuePair <字符串,字符串>>,然后我们使用Microsoft.AspNetCore.Http的KeyValuePair <字符串,字符串>代替HttpCookie。
    命名空间替换:
    • 选择列表🡪Microsoft.AspNetCore.Mvc.Rendering
    • UrlHelper🡪WebUtitlity
    • MimeMapping🡪FileExtensionContentTypeProvider
    • MvcHtmlString🡪IHtmlString和HtmlString
    • ModelState,ModelStateDictionary,ModelError和Microsoft.AspNetCore.Mvc.ModelBinding
    • FormCollection🡪IFormCollection
    • Request.Url.Scheme🡪this.Url.ActionContext.HttpContext.Request.Scheme
    其他:
    • MvcHtmlString.IsNullOrEmpty(IHtmlString)🡪String.IsNullOrEmpty(variable.ToHtmlString())
    • [ValidateInput(false)]-不再存在,不再需要
    • HttpUnauthorizedResult🡪UnauthorizedResult
    • [AllowHtml]-指令不再存在,不再需要
    • TagBuilder.SetInnerText方法被InnerHtml.AppendHtml取代
    • 返回Json时不再需要JsonRequestBehavior.AllowGet
    • HttpUtility.JavaScriptStringEncode。 JavaScriptEncoder.Default.Encode
    • Request.RawUrl。 Request.Path + Request.QueryString应该分开连接
    • AllowHtmlAttribute-类不再存在
    • XmlDownloadResult-现在您可以使用返回File(Encoding.UTF8.GetBytes(xml),“ application / xml”,“ filename.xml”);
    • [ValidateInput(false)]-指令不再存在,不再需要

    步骤13.身份验证和授权更新

    如上所述,nopCommerce项目不涉及内置的身份验证系统,它是在单独的中间件层中实现的。

    但是,ASP.NET Core具有自己的凭据提供系统。 您可以查看 文档 以详细了解它们。

    至于数据保护,我们不再使用MachineKey 。 相反,我们使用内置的数据保护功能。 默认情况下,启动应用程序时会生成密钥。 由于数据存储可以是:

    • 文件系统-基于文件系统的密钥库
    • Azure存储-Azure BLOB对象存储中的数据保护密钥
    • Redis-Redis缓存中的数据保护密钥
    • 注册表-如果应用程序无权访问文件系统,则使用
    • EF Core-密钥存储在数据库中

    如果内置提供程序不合适,则可以通过创建自定义IXmlRepository来指定自己的密钥存储提供程序。

    步骤14. JS / CSS更新

    静态资源的使用方式已更改,现在,除非进行了其他设置,否则所有静态资源都应存储在项目wwwroot的根文件夹中。

    使用javascript内置块时,建议将其移至页面末尾。 只需对<script>标签使用asp-location =“ Footer”属性。 相同的规则适用于js文件。

    使用BundlerMinifier扩展名替代System.Web.Optimization-这将启用捆绑和缩小。 构建项目时使用JavaScript和CSS(请参阅文档 )。

    步骤15.移植视图

    首先,不再使用子操作,而ASP.NET Core建议使用新的高性能工具-异步调用的ViewComponents 。

    如何从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();
        }
    }
               
    请注意,不再需要使用HtmlHelper,ASP.NET Core包含许多内置的辅助Tag Helpers 。 当应用程序运行时,Razor引擎在服务器上处理它们,并最终转换为标准html元素。
    这使应用程序开发变得非常容易。 当然,您可以实现自己的标签助手。

    我们开始在视图中使用依赖项注入,而不是使用EngineContext启用设置和服务。

    因此,有关移植视图的要点如下:

    • 将Views / web.config转换为Views / _ViewImports.cshtml-导入名称空间并注入依赖项。 此文件不支持其他Razor功能,例如功能和节定义
    • 将namespaces.add转换为@using
    • 将任何设置移植到主应用程序配置
    • Scripts.Render和Styles.Render不存在。 替换为指向libman或BundlerMinifier的输出数据的链接

    结论

    迁移大型Web应用程序的过程非常耗时,通常,如果没有陷阱,就无法执行该任务。 我们计划在第一个稳定版本发布后立即迁移到新框架,但由于有一些关键功能尚未转移到.NET Core,特别是与EntityFramework相关的功能,因此无法立即将其迁移到新框架。 。

    因此,我们必须首先使用混合方法发布-具有.NET Framework依赖项的.NET Core体系结构,这本身就是一个独特的解决方案。

    成为第一并不容易,但是我们确信我们做出了正确的选择,我们庞大的社区为此提供了支持。

    在.NET Core 2.1发布之后,我们能够完全适应我们的项目,到那时,已经有一个稳定的解决方案已经可以在新体系结构上工作了。 剩下的只是替换一些软件包并用EF Core重写工作。

    因此,我们花了几个月时间和两个发行版本才能完全迁移到新框架。

    我们可以充满信心地说,我们是第一个 进行此类迁移的 大型项目 。 在本指南中,我们试图以结构化的形式汇总整个迁移过程,并描述各种瓶颈,以便其他开发人员可以依靠此材料并在解决相同任务时遵循路线图。

From: https://hackernoon.com/how-to-migrate-project-from-aspnet-mvc-to-aspnet-core-qt1ks31zn