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

2019-07-08 19:14:49 浏览数 (1)

现在越来越多的人在谈论. 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”目录之外。

然后像这样使用

代码语言:javascript复制
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:

代码语言:javascript复制
<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. 使用构造函数注入初始化它

代码语言:javascript复制
public SomeController(IHttpContextAccessor accessor)
{
    _accessor = accessor;
}

3. 获取客户端IP地址

代码语言:javascript复制
_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 代码爆掉。

例如以下代码:

代码语言:javascript复制
public IActionResult JsonTest()
{
    return Json(new { Foo = 1, Goo = true, Koo = "Test" });
}

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

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

代码语言:javascript复制
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 来实现:

代码语言:javascript复制
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"的文件,内容如下:

代码语言:javascript复制
<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到输出目录,不然无效!

代码语言:javascript复制
<ItemGroup>
  <None Update="UrlRewrite.xml">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </None>
</ItemGroup>

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

代码语言:javascript复制
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 文件如下:

代码语言:javascript复制
<?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%LogFilesstdout" />
    </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 支持。

代码语言:javascript复制
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(20);
    options.Cookie.HttpOnly = true;
});

以及

代码语言:javascript复制
app.UseSession();

设定和获取Session值:

代码语言:javascript复制
HttpContext.Session.SetString("CaptchaCode", result.CaptchaCode);
HttpContext.Session.GetString("CaptchaCode");

清除值:

代码语言:javascript复制
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 签名的:

代码语言:javascript复制
async Task<IViewComponentResult> InvokeAsync()

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

代码语言:javascript复制
await Task.CompletedTask;

14

检查运行环境是 Debug 或 Release

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

代码语言:javascript复制
@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文件自动增加版本字符串:

代码语言:javascript复制
<script src="~/js/app/ediblog.app.min.js" asp-append-version="true"></script>

它的结果是:

代码语言:javascript复制
<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 以及字段的名字了。

代码语言:javascript复制
services.AddAntiforgery(options =>
{
    options.Cookie.Name = "X-CSRF-TOKEN-MOONGLADE";
    options.FormFieldName = "CSRF-TOKEN-MOONGLADE-FORM";
});

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

代码语言:javascript复制
@Html.AntiForgeryToken()

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

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

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

代码语言:javascript复制
services.AddMvc(options =>
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));

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

代码语言:javascript复制
[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller

17

对非Controller 使用依赖注入

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

代码语言:javascript复制
public class HomeController : Controller
{
    private readonly IDateTime _dateTime;
    public HomeController(IDateTime dateTime)
    {
        _dateTime = dateTime;
    }
}

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

代码语言:javascript复制
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 中会抛出异常:

代码语言:javascript复制
var buffer = new byte[context.Request.Body.Length];
context.Request.Body.Read(buffer, 0, buffer.Length);
var xml = Encoding.Default.GetString(buffer);

它的结果是:

代码语言:javascript复制
System.NotSupportedException: Specified method is not supported.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.get_Length()

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

代码语言:javascript复制
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

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

代码语言:javascript复制
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文件:

代码语言:javascript复制
<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给我交流你的发现。

0 人点赞