1. 程式人生 > >將傳統 ASP.NET 應用遷移到 .NET Core

將傳統 ASP.NET 應用遷移到 .NET Core

點選藍字

關注我

現在越來越多的人在談論. NET Core。誠然,.NET Core 是未來, 但是.NET Framework 仍在支援, 因為大量的應用程式無法在短時間內遷移。

.NET Core 和 .NET Framework 就像電動汽車和汽油動力汽車。汽油車是成熟的,你可以毫無任何問題駕駛它,但電動車有它們的優勢,並正在取代汽油車。所以,不要誤會,你應該從今天開始遷移到. NET Core。

長文預警

640?wx_fmt=gif

我已經遷移了幾個執行在完整.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上。 smiley_13.png

640?wx_fmt=gif1

遷移或重寫

有時候,我更喜歡用“重寫“而不是”遷移“這個詞,因為在有些情況下,.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。

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,你應該選擇使用這樣的版本。

640?wx_fmt=png

你依然可以在.NET Core工程裡引用一個.NET Framework的包,但是這會讓你的應用只能跑在Windows上smiley_0.png,不推薦這麼做。

我列出了一些熱門使用的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

640?wx_fmt=png

3

客戶端包管理

ASP.NET Core 曾經使用 Bower 去管理客戶端包。但在最新的ASP.NET Core 2.1 裡,Bower 已經被移除了,因為作者不幹了smiley_11.png。因此,微軟預設使用自家的包管理器 “Library Manager” 也叫 “libman” 去管理前端包。它能夠在 Visual StudioVisual Studio Code 中使用,甚至也能用 CLI 在命令列下使用。

640?wx_fmt=png

libman.json 可以直接編輯,也能在UI中更改,都有智慧感知支援。我的建議是,如果你的應用不是重客戶端的話,使用 libman 去管理前端包,因為其他技術比如NPM 太重量級了。你會希望在你的編譯伺服器上安裝和配置NodeJS以及其他一切東西,僅僅為了拉取一個jQuery 庫。

更多詳情可參見官方文件 https://docs.microsoft.com/en-us/aspnet/core/client-side/libman/?view=aspnetcore-2.1

640?wx_fmt=png

4

Html / JavaScript / CSS

你可以直接將這些檔案複製到.NET Core工程裡。但是請確保你已經把檔案路徑修改正確,比如CSS裡的圖片檔案路徑。因為傳統ASP.NET / MVC 模板預設使用 “/Content/” 目錄,而.NET Core模板使用“/css/”, “/js/”, “/lib/” 等目錄,這並不是強制的,只是約定俗成的規範。

640?wx_fmt=png

如果你希望捆綁並壓縮CSS 和JS 檔案,有許多工具可以辦到。我個人喜歡用VS的一款外掛,叫做 “Bundler & Minifier” ,你可以從這裡獲取https://github.com/madskristensen/BundlerMinifier.

這款外掛可以在開發時生成捆綁及壓縮的檔案,但非編譯或執行時。

640?wx_fmt=gif

5

App_Data 資料夾

在傳統ASP.NET/MVC 應用中,你可以將資料檔案儲存到一個名為“App_Data”的特殊資料夾中,但這個東西在.NET Core裡不復存在了。為了實現類似的功能,你需要自己建立一個名為“App_Data” 的資料夾,但位於“wwwroot”目錄之外。

640?wx_fmt=png

然後像這樣使用

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";

}

640?wx_fmt=gif

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

640?wx_fmt=png

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不太一樣,對我們更加有用一些。

640?wx_fmt=png

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

@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor

使用方法:

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

640?wx_fmt=gif

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給客戶端:

640?wx_fmt=png

如果你有大量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 的結果:

640?wx_fmt=png640?wx_fmt=gif1

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

640?wx_fmt=png

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到輸出目錄,不然無效!smiley_11.png

<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

640?wx_fmt=png

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

640?wx_fmt=gif

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

640?wx_fmt=png

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 簽名的smiley_11.png

async Task<IViewComponentResult> InvokeAsync()

但如果你的程式碼並不是天生非同步的,為了不讓編譯器警報,你可以加入這行程式碼:

await Task.CompletedTask;

640?wx_fmt=png

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 的用法。

640?wx_fmt=png

15

新的Razor Tag Helpers

Tag helper 可以幫助你講老的HTML helper 簡化為更加面向HTML可讀的程式碼,例如一個表單,我們曾經要這樣寫:

640?wx_fmt=png

轉換為 Tag Helpers 的結果是這樣的:

640?wx_fmt=png

我個人最喜歡的功能是給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

640?wx_fmt=png

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 欄位會自動在輸出到客戶端時自動加上。

640?wx_fmt=png

但你依然需要在後臺對應的Action上加上 [ValidateAntiForgeryToken] 屬性。

然而,有另一種自動給每一個POST請求都驗證anti-forgery token 的辦法。

services.AddMvc(options =>

options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));

或者你可以單獨給一個 Controller 加上這個屬性。

[Authorize]

[AutoValidateAntiforgeryToken]

public class ManageController : Controller

640?wx_fmt=png

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

640?wx_fmt=png

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();

640?wx_fmt=gif

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 。

640?wx_fmt=png

20

熱更新 Views

在傳統 ASP.NET MVC 中,Views 資料夾預設不會編譯到 DLL 檔案中,所以我們能夠不需要編譯整個應用就能更新razor頁面。這在不需要更新C#程式碼的情況下僅修改文字或一些layout修改的場景下非常實用。我有時候也利用這個特性直接向生產環境釋出一些修改後的頁面。

640?wx_fmt=png

然而,ASP.NET Core 2.1 預設情況下會將我們的 Views 編譯到DLL 中以提高效能。因此,你無法在伺服器上直接修改一個檢視,因為資料夾中根本就不存在 Views,只有一個 *.Views.dll:

640?wx_fmt=png

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

<PropertyGroup>

  <TargetFramework>netcoreapp2.1</TargetFramework>

  <RazorCompileOnBuild>false</RazorCompileOnBuild>

  <RazorCompileOnPublish>false</RazorCompileOnPublish>

</PropertyGroup>

640?wx_fmt=gif

21

編譯版本號自增長

在傳統 .NET 應用程式裡,我們可以修改 “AssemblyInfo.cs” 在每次編譯時自動增加版本號。這在編譯伺服器裡十分常用。

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

結果是這樣:

9.0.6836.29475

不幸的是,.NET Core 目前還沒有一個自帶的方法來完成這個操作。只有一個三方解決方案可能有用:https://github.com/BalassaMarton/MSBump

640?wx_fmt=gif

能看到這裡的都是我的真愛粉啊……

結束

ASP.NET Core 相對傳統 ASP.NET 有了不少區別,目前也有一定的限制。本文僅涵蓋了我自己所遇到的問題,也一定還有很多我沒有遇到過的情況。歡迎留言或Email給我交流你的發現。