首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >将传统 ASP.NET 应用迁移到 .NET Core

将传统 ASP.NET 应用迁移到 .NET Core

作者头像
Edi Wang
发布2019-07-08 19:14:49
4.4K1
发布2019-07-08 19:14:49
举报
文章被收录于专栏:汪宇杰博客汪宇杰博客

现在越来越多的人在谈论. NET Core。诚然,.NET Core 是未来, 但是.NET Framework 仍在支持, 因为大量的应用程序无法在短时间内迁移。

.NET Core 和 .NET Framework 就像电动汽车和汽油动力汽车。汽油车是成熟的,你可以毫无任何问题驾驶它,但电动车有它们的优势,并正在取代汽油车。所以,不要误会,你应该从今天开始迁移到. NET Core。

我已经迁移了几个运行在完整.NET Framework和IIS上的传统ASP.NET/MVC项目到ASP.NET Core 2.x,可以运行在IIS或非IIS环境下。

我的博客是其中之一。这是一个有10年历史的博客系统,最初由 ASP.NET 2.0 Web Form以及Visual Basic编写。从2008年起,我一直在面向最新的.NET技术更新代码库。.NET Core版本的博客系统将在今年年底到来。我写这篇文章,记录我遇到的路障和如何解决它们的方法。

这篇文章针对的是新接触.NET Core,但有.NET Framework经验的开发人员,帮助他们将现有的应用更平滑的过渡到.NET Core上。

1

迁移或重写

有时候,我更喜欢用“重写“而不是”迁移“这个词,因为在有些情况下,.NET Core和.NET Framework是完全不同的两个东西。

根据我的经验,大部分前端代码可以只做少量修改就直接移植到.NET Core,因为它们的本质毕竟是服务器技术无关的,天生跨平台的技术。至于后端代码,迁移成本取决于它们对Windows及IIS的耦合程度。我理解,有些应用会充分利用Windows 及 IIS 的特性,这样开发者就可以避免自己费力去实现一些功能。这些包括计划任务、注册表、活动目录或Windows服务等。这些并不能够直接迁移,因为.NET Core是跨平台的。对于这些部分,你可能需要考虑从重新设计业务逻辑,想一种可以实现相同功能,但不依赖于Windows 或IIS 组件的方法。

对于无法迁移的历史遗留代码,你可能需要考虑重新设计整个应用的架构,将这些功能作为REST API暴露出来,可以使用.NET Framework上的ASP.NET Web API来实现。这样的话,你的ASP.NET Core 应用得以继续使用这些API并继续完成业务功能。

如果你的应用使用了WCF服务,甚至更老的 ASMX 服务,这可能就没法搞了。因为.NET Core目前还不支持调用WCF。除非你能更新你的WCF 服务去暴露 REST 协议。但是 REST 和WCF 并不是功能完全一致的,比如双工通信。在某些场合下,你需要在应用层迁移到.NET Core之前,面向REST 重新设计你的API。

2

NuGet 包管理

请确保你需要使用的NuGet包支持 .NET Core 或 .NET Standard。如果不支持,那么你需要研究有没有可以替换的NuGet包,或者你是否能够自己写代码去实现相同的功能。

.NET Standard 意味着这个包可以同时使用在.NET Framework 4.6.1+ 以及.NET Core,这是取代老的 Portable Class Library (PCL)的技术。所以,如果你看到一个包的依赖项里有.NET Standard,这意味着你能够将它安装到你的.NET Core工程中。

部分包,比如NLog有专门的.NET Core版本,比如 NLog.Web.AspNetCore,你应该选择使用这样的版本。

你依然可以在.NET Core工程里引用一个.NET Framework的包,但是这会让你的应用只能跑在Windows上

,不推荐这么做。

我列出了一些热门使用的NuGet 包,它们都已经支持.NET Core:

NLog.Web.AspNetCore

Newtonsoft.Json

HtmlAgilityPack

RestSharp

NUnit

Dapper

AutoMapper

Moq

对于客户端包,比如 jQuery,请不要使用NuGet 将它们安装到.NET Core工程中,参见本文的 “客户端包管理” 章节。

如果你使用 Visual Studio Code 做 .NET Core 开发,请注意,安装NuGet包的命令不是 Install-Package,那是给Visual Studio的 PowerShell host用的,在VSCode里,你需要使用dotnet CLI工具,比如:

dotnet add package Newtonsoft.Json

参见 https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-add-package

3

客户端包管理

ASP.NET Core 曾经使用 Bower 去管理客户端包。但在最新的ASP.NET Core 2.1 里,Bower 已经被移除了,因为作者不干了

。因此,微软默认使用自家的包管理器 “Library Manager” 也叫 “libman” 去管理前端包。它能够在 Visual StudioVisual Studio Code 中使用,甚至也能用 CLI 在命令行下使用。

libman.json 可以直接编辑,也能在UI中更改,都有智能感知支持。我的建议是,如果你的应用不是重客户端的话,使用 libman 去管理前端包,因为其他技术比如NPM 太重量级了。你会希望在你的编译服务器上安装和配置NodeJS以及其他一切东西,仅仅为了拉取一个jQuery 库。

更多详情可参见官方文档 https://docs.microsoft.com/en-us/aspnet/core/client-side/libman/?view=aspnetcore-2.1

4

Html / JavaScript / CSS

你可以直接将这些文件复制到.NET Core工程里。但是请确保你已经把文件路径修改正确,比如CSS里的图片文件路径。因为传统ASP.NET / MVC 模板默认使用 “/Content/” 目录,而.NET Core模板使用“/css/”, “/js/”, “/lib/” 等目录,这并不是强制的,只是约定俗成的规范。

如果你希望捆绑并压缩CSS 和JS 文件,有许多工具可以办到。我个人喜欢用VS的一款插件,叫做 “Bundler & Minifier” ,你可以从这里获取https://github.com/madskristensen/BundlerMinifier.

这款插件可以在开发时生成捆绑及压缩的文件,但非编译或运行时。

5

App_Data 文件夹

在传统ASP.NET/MVC 应用中,你可以将数据文件保存到一个名为“App_Data”的特殊文件夹中,但这个东西在.NET Core里不复存在了。为了实现类似的功能,你需要自己创建一个名为“App_Data” 的文件夹,但位于“wwwroot”目录之外。

然后像这样使用

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // set
    string baseDir = env.ContentRootPath;
    AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(baseDir, "App_Data"));
    // use
    var feedDirectoryPath = $"{AppDomain.CurrentDomain.GetData("DataDirectory")}\\feed";
}

6

自定义 Http Headers

在传统ASP.NET里,你可以在Web.Config 里像这样为每个响应都配置自定义的HTTP Header:

<httpProtocol>
  <customHeaders>
    <add name="X-Content-Type-Options" value="nosniff" />
  </customHeaders>
</httpProtocol>

而在.NET Core里,如果你希望脱离Windows去部署你的应用,不可以使用Web.config文件。因此,你需要一个三方的 NuGet 包来完成这个功能:NetEscapades.AspNetCore.SecurityHeaders

app.UseSecurityHeaders(new HeaderPolicyCollection()

.AddCustomHeader("X-UA-Compatible", "IE=edge")

.AddCustomHeader("X-Developed-By", "Edi Wang")

);

详情参考 https://github.com/andrewlock/NetEscapades.AspNetCore.SecurityHeaders

7

获取客户端IP地址以及 HttpContext

在传统ASP.NET 里,我们能够通过 Request.UserHostAddress 来获取客户端IP地址。但这个属性在 ASP.NET Core 2.x 里是不存在的。我们需要通过另一种方式获取HTTP 请求信息。

1. 在你的 MVC 控制器里定义一个私有变量

private IHttpContextAccessor _accessor;

2. 使用构造函数注入初始化它

public SomeController(IHttpContextAccessor accessor)
{
    _accessor = accessor;
}

3. 获取客户端IP地址

_accessor.HttpContext.Connection.RemoteIpAddress.ToString()

就是如此简单。

如果你的 ASP.NET Core 工程是用MVC默认模板创建的,针对HttpContextAcccessor 依赖注入注册应该在Startup.cs 中完成:

services.AddHttpContextAccessor();

services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();

RemoteIpAddress 的类型是 IPAddress 并不是string。它包含 IPv4, IPv6 以及其他信息。这和传统ASP.NET不太一样,对我们更加有用一些。

如果你希望在Razor 视图(cshtml) 里使用,只需要用 @inject 指令注入到view中:

@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor

使用方法:

Client IP: @HttpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString()

8

JsonResult

默认情况下,ASP.NET Core 会使用 camelCase 序列化 JsonResult ,而传统 ASP.NET MVC 使用的是PascalCase,这会导致依赖Json结果的 JavaScript 代码爆掉。

例如以下代码:

public IActionResult JsonTest()
{
    return Json(new { Foo = 1, Goo = true, Koo = "Test" });
}

它会返回camelCase 的Json给客户端:

如果你有大量JavaScript 代码并不能及时改为使用camelCase,你仍然可以配置 ASP.NET Core 向客户端输出 PascalCase 的Json

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc()
        .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
}

现在,之前的代码会返回PascalCase 的结果:

1

HttpModules 和 HttpHandlers

这两者在ASP.NET Core中被替换为了 Middleware。但在迁移之前,你可以考虑使用别的方法,在一个普通ASP.NET Core Controller 中实现这些功能。

例如,我的老博客系统里有个名为“opml.axd” 的HttpHandler 作用是向客户端输出一个XML文档,这其实完全可以用 Controller 来实现:

public async Task<IActionResult> Index()
{
    var opmlDataFile = $"{AppDomain.CurrentDomain.GetData(Constants.DataDirectory)}\\opml.xml";
    if (!System.IO.File.Exists(opmlDataFile))
    {
        Logger.LogInformation($"OPML file not found, writing new file on {opmlDataFile}");
        await WriteOpmlFileAsync(HttpContext);
        if (!System.IO.File.Exists(opmlDataFile))
        {
            Logger.LogInformation($"OPML file still not found, something just went very very wrong...");
            return NotFound();
        }
    }
    string opmlContent = await Utils.ReadTextAsync(opmlDataFile, Encoding.UTF8);
    if (opmlContent.Length > 0)
    {
        return Content(opmlContent, "text/xml");
    }
    return NotFound();
}

我也曾经使用HttpHandler 完成Open Search,RSS/Atom等功能,它们也能够被 重写为Controller。

对于其他一些不能够被重写为MVC Controller的组件,例如处理特殊拓展名的请求。请参见:

https://docs.microsoft.com/en-us/aspnet/core/migration/http-modules?view=aspnetcore-2.1

10

IIS URL Rewrite

你依然可以使用和旧应用里完全一样的配置文件,不管你的 .NET Core 应用是否部署在IIS上。

例如,在应用根目录底下创建一个名为"UrlRewrite.xml"的文件,内容如下:

<rewrite>
  <rules>
    <rule name="Redirect Misc Homepage URLs to canonical homepage URL" stopProcessing="false">
      <match url="(index|default).(aspx?|htm|s?html|php|pl|jsp|cfm)"/>
      <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
        <add input="{REQUEST_METHOD}" pattern="GET"/>
      </conditions>
      <action type="Redirect" url="/"/>
    </rule>
  </rules>
</rewrite>

注意:你必须把这个文件设置为always copy到输出目录,不然无效!

<ItemGroup>
  <None Update="UrlRewrite.xml">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </None>
</ItemGroup>

打开 Startup.cs,在Configure 方法中添加如下代码:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    ...
    using (var urlRewriteStreamReader = File.OpenText("UrlRewrite.xml"))
    {
        var options = new RewriteOptions().AddIISUrlRewrite(urlRewriteStreamReader);
        app.UseRewriter(options);
    }
    ...
}

这在我之前的文章中提到过https://edi.wang/post/2018/9/18/prevent-image-hotlink-aspnet-core.

更多选项和用法可以参考 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/url-rewriting?view=aspnetcore-2.1

11

Web.config

Web.config 文件并没有完全消亡。在 In .NET Core 里,一个 web.config 文件仍然用于在IIS环境下部署网站。在这种场景下,Web.config 里的配置仅作用于 IIS,和你的应用代码没有任何关系。可以参考 https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/index?view=aspnetcore-2.1#configuration-of-iis-with-webconfig

一个典型的IIS下部署ASP.NET Core应用的web.config 文件如下:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet" arguments=".\Moonglade.Web.dll" stdoutLogEnabled="false" stdoutLogFile="\\?\%home%\LogFiles\stdout" />
    </system.webServer>
  </location>
</configuration>

曾经的 AppSettings 节点可迁移到 appsettings.json,在这篇文章中有详解:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-2.1

12

Session 和 Cookie

ASP.NET Core 默认没有开启Session支持,你必须手工添加Session 支持。

services.AddDistributedMemoryCache();
services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(20);
    options.Cookie.HttpOnly = true;
});

以及

app.UseSession();

设定和获取Session值:

HttpContext.Session.SetString("CaptchaCode", result.CaptchaCode);
HttpContext.Session.GetString("CaptchaCode");

清除值:

context.Session.Remove("CaptchaCode");

详情参见:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-2.1

13

Html.Action

我们曾经使用 Html.Action 去调用一个Action ,返回一个Partial View ,然后放在主要的View 中显示,比如layout页。这在Layout页面中的应用非常广泛,比如在一个博客系统中显示分类列表之类的小部件。

@Html.Action("GetTreeList", "Category")

在ASP.NET Core里,它被替换为了 ViewComponents,参见 https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components

一个要注意的地方是Invoke方法只能是 async 签名的:

async Task<IViewComponentResult> InvokeAsync()

但如果你的代码并不是天生异步的,为了不让编译器警报,你可以加入这行代码:

await Task.CompletedTask;

14

检查运行环境是 Debug 或 Release

在我的老系统里,我使用 HttpContext.Current.IsDebuggingEnabled 去检查当前运行环境是否为Debug,并在标题栏上显示 “(Debug)” 字样。

@if (HttpContext.Current.IsDebuggingEnabled)
{
    <text>(Debug)</text>
}
在 ASP.NET Core 里,我们可以使用新的razor tag helper 去完成这件事
<environment include="Development">
    (Debug)
</environment>

在下面的章节里,你会看到更多razor tag helper 的用法。

15

新的Razor Tag Helpers

Tag helper 可以帮助你讲老的HTML helper 简化为更加面向HTML可读的代码,例如一个表单,我们曾经要这样写:

转换为 Tag Helpers 的结果是这样的:

我个人最喜欢的功能是给JS或CSS文件自动增加版本字符串:

<script src="~/js/app/ediblog.app.min.js" asp-append-version="true"></script>

它的结果是:

<script src="/js/app/ediblog.app.min.js?v=lvNJVuWBoD_RVZwyBT15T_i3_ZuEIaV_w0t7zI_UYxY"></script>

新的razor 语法能够兼容以前的 HTML helpers,也就是说,你依然能在ASP.NET Core中毫无问题的使用老的 HTML helpers。如果你的应用迁移时间紧迫,你可以尽管先使用老代码,随后再逐步转换到Tag Helpers。

完整的介绍和语法列表,可参见https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-2.1

16

Anti-Forgery Token

Anti-forgery token 有一些改进。首先,你能够自定义cookie 以及字段的名字了。

services.AddAntiforgery(options =>
{
    options.Cookie.Name = "X-CSRF-TOKEN-MOONGLADE";
    options.FormFieldName = "CSRF-TOKEN-MOONGLADE-FORM";
});

第二,你再也不需要手工给每一个表单都增加这行代码了:

@Html.AntiForgeryToken()

如果你使用新的form tag helper,那么anti-forgery 字段会自动在输出到客户端时自动加上。

但你依然需要在后台对应的Action上加上 [ValidateAntiForgeryToken] 属性。

然而,有另一种自动给每一个POST请求都验证anti-forgery token 的办法。

services.AddMvc(options =>
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));

或者你可以单独给一个 Controller 加上这个属性。

[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller

17

对非Controller 使用依赖注入

ASP.NET Core 有自带的 DI 框架可以用在 Controller 上。我们可以修改一个Controller 的构造函数去注入它运行所依赖的服务。

public class HomeController : Controller
{
    private readonly IDateTime _dateTime;
    public HomeController(IDateTime dateTime)
    {
        _dateTime = dateTime;
    }
}

但这不意味着自带的DI框架只能用在Controller 上。对于其他类,你可以使用完全一样的DI,例如,我自定义的类,也可以使用构造函数注入:

public class CommentService : MoongladeService
{
    private readonly EmailService _emailService;
    public CommentService(MoongladeDbContext context,
        ILogger<CommentService> logger,
        IOptions<AppSettings> settings,
        EmailService emailService) : base(context, logger, settings)
    {
        _emailService = emailService;
    }
       // ....
}

方法是,只要你把自定义的类注册到Startup.cs中的 DI 容器里即可。

services.AddTransient<CommentService>();

更多ASP.NET Core 依赖注入的使用方法参见 https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/dependency-injection?view=aspnetcore-2.1

18

API 行为不一致

有些来自传统 ASP.NET 的代码可以无错误编译通过,但这不保证运行时能够成功。比如,这段来自ASP.NET (.NET Framework) 的代码在 ASP.NET Core 中会抛出异常:

var buffer = new byte[context.Request.Body.Length];
context.Request.Body.Read(buffer, 0, buffer.Length);
var xml = Encoding.Default.GetString(buffer);

它的结果是:

System.NotSupportedException: Specified method is not supported.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.get_Length()

在.NET Core里,我们需要用一种不同的方式去实现:

var xml = await new StreamReader(context.Request.Body, Encoding.Default).ReadToEndAsync();

19

小心GDPR 带来的问题

ASP.NET Core 2.1 默认添加了 GDPR 的支持,但也会给我们带来一些问题。关于GDPR可参见 https://docs.microsoft.com/en-us/aspnet/core/security/gdpr?view=aspnetcore-2.1

主要问题是,在用户接受GDPR协议之前,Cookie 是不起作用的。你需要检查哪些Cookie是你应用运行所必须的,即时用户没有接受GDPR协议,并且把它们标记为IsEssential

这是我博客系统中的一个例子:

private void SetPostTrackingCookie(CookieNames cookieName, string id)
{
    var options = new CookieOptions
    {
        Expires = DateTime.UtcNow.AddDays(1),
        SameSite = SameSiteMode.Strict,
        Secure = Request.IsHttps,
        // Mark as essential to pass GDPR
        // https://docs.microsoft.com/en-us/aspnet/core/security/gdpr?view=aspnetcore-2.1
        IsEssential = true
    };
    Response.Cookies.Append(cookieName.ToString(), id, options);
}

另一个问题是,如果你要使用Session,那么用户必须接受GDPR 策略,否则 Session是不工作的。因为 Session 需要依赖 Cookie 在客户端保存 SessionID 。

20

热更新 Views

在传统 ASP.NET MVC 中,Views 文件夹默认不会编译到 DLL 文件中,所以我们能够不需要编译整个应用就能更新razor页面。这在不需要更新C#代码的情况下仅修改文字或一些layout修改的场景下非常实用。我有时候也利用这个特性直接向生产环境发布一些修改后的页面。

然而,ASP.NET Core 2.1 默认情况下会将我们的 Views 编译到DLL 中以提高性能。因此,你无法在服务器上直接修改一个视图,因为文件夹中根本就不存在 Views,只有一个 *.Views.dll:

如果你仍然希望在ASP.NET Core中热更新Views,需要手工修改csproj文件:

<PropertyGroup>
  <TargetFramework>netcoreapp2.1</TargetFramework>
  <RazorCompileOnBuild>false</RazorCompileOnBuild>
  <RazorCompileOnPublish>false</RazorCompileOnPublish>
</PropertyGroup>

21

编译版本号自增长

在传统 .NET 应用程序里,我们可以修改 “AssemblyInfo.cs” 在每次编译时自动增加版本号。这在编译服务器里十分常用。

[assembly: AssemblyVersion("9.0.*")]

结果是这样:

9.0.6836.29475

不幸的是,.NET Core 目前还没有一个自带的方法来完成这个操作。只有一个三方解决方案可能有用:https://github.com/BalassaMarton/MSBump

能看到这里的都是我的真爱粉啊……

结束

ASP.NET Core 相对传统 ASP.NET 有了不少区别,目前也有一定的限制。本文仅涵盖了我自己所遇到的问题,也一定还有很多我没有遇到过的情况。欢迎留言或Email给我交流你的发现。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-10-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 汪宇杰博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档