Porting Assistant for .NETを使ってASP.NET Web FormsアプリケーションをBlazorへ移植する

2021.12.25

いわさです。

本記事は、Blazor Advent Calendar 2021の25日目です。

ASP.NET Webアプリケーションのモダナイゼーションを支援するためのツールとしてAWSよりPorting Assistant for .NETというスタンドアロンツールが無償提供されています。
このツールは、以前以下の記事で、ASP.NET MVCをASP.NET MVC Coreへ移行した際に紹介させて頂いていまして、AWSでアプリケーションを動かすわけでなくても利用できます。(Azureでも良い)

そのツールが今回アップデートされまして、最新バージョンでは.NET6.0への移植に加えて、なななんとASP.NET Web FormsアプリケーションをASP.NET Core Blazorへ移植する機能が追加されました。
もちろん、Porting Assistantはまったくの修正いらずで移植を行えるというものではないのですが、移植に必要な手作業の大部分をツールに任せることが出来ます。

本日は実際にASP.NET Web Formsアプリケーションを移植し、調整のうえ実行させるところまでやってみました。

分析と移植

Porting Assistantの役割として大きく2つあります。
ひとつが分析です。ライブラリなどの互換性の評価をしてくれます。
ふたつめが変換(移植)です。

今回は新規のASP.NET Web Formsアプリケーションを作成し、デフォルトの状態で移植を行いました。
実際の現場では様々なライブラリの互換性対応が必要になるんじゃないかなと思いますのでその点ご注意ください。

はい。いつもの画面ですね。

ではツールを使っていきましょう。
冒頭の記事を参考にPorting AssistantのインストールとAWSプロファイルの設定が済んでいる前提で話を進めます。

まず、Settingsよりアプリケーションバージョンが最新(本日時点で1.6.3)でターゲットフレームワークが.NET6.0であることを確認してください。
そう、.NET6.0に移植するので、.NET SDKもインストールしておいてください。

.NET SDKs downloads for Visual Studio

ターゲットが異なる場合は以下より変更しましょう。

分析とレポートの確認方法については、前回の記事と同じ流れなので割愛します。
分析が出来たら移植すると、出力先フォルダに変換後のソリューションファイル一式が出力されます。

移植後に調整

ここまでの操作でBlazor Serverアプリケーションに変換されています。
ホストページが用意されブートストラップ用のコンポーネントやスクリプトが配置されていますね。

ただし、このままビルドして実行、としても動作しません。
コードの調整が若干必要ですので見ていきたいと思います。

.NET SDKの変更

まず、移植後のプロジェクトを開くと最初に違和感を感じると思います。
wwwrootが通常のフォルダとして表示されていて、よくみるとページファイルもビルドアクション「なし」になっています。

csprojを開き、SDKを変更しましょう。

.NET プロジェクト SDK の概要 | Microsoft Docs

プロジェクトSDKが移植後はMicrosoft.NET.Sdkになっています。
こちらをWebSDK(Microsoft.NET.Sdk.Web)に変更します。

hogehoge.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <OutputType>Exe</OutputType>
    <UseRazorSourceGenerator>false</UseRazorSourceGenerator>
  </PropertyGroup>
  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="bootstrap" Version="4.0.0" />
    <PackageReference Include="jQuery" Version="3.5.0" />
    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
  </ItemGroup>

  <ItemGroup Label="PortingInfo">
  <!-- DO NOT REMOVE WHILE PORTING -->
  </ItemGroup>
</Project>

変更後に、色々とまたエラーが出るので潰していきます。
なお、ここを変更しないとそもそもKestrelで実行後にアクセスしても以下のようなエラーが発生します。

info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[63]
      User profile is available. Using 'C:\Users\iwasa.takahito\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Users\iwasa.takahito\source\repos\portedaspdotnetwebform20211222\aspdotnetwebform20211222\bin\Debug\net6.0
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://localhost:5000/ - -
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HME4OD6865CK", Request id "0HME4OD6865CK:00000002": An unhandled exception was thrown by the application.
      System.InvalidOperationException: Cannot find the fallback endpoint specified by route values: { page: /_Host, area:  }.
         at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.DynamicPageEndpointMatcherPolicy.ApplyAsync(HttpContext httpContext, CandidateSet candidates)
         at Microsoft.AspNetCore.Routing.Matching.DfaMatcher.SelectEndpointWithPoliciesAsync(HttpContext httpContext, IEndpointSelectorPolicy[] policies, CandidateSet candidateSet)
         at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.<Invoke>g__AwaitMatch|8_1(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/1.1 GET http://localhost:5000/ - - - 500 0 - 286.6748ms

ViewSwitcherの無効化

ASP.NETでは、ViewSwitcherを使って、モバイル用のレイアウトに切り替えることが出来ます。
ViewSwitcherも他のページファイルと同様にRazorファイルとして変換されているのですが、コンポーネントの解決エラーが発生するのでここは今回は無効化して対応しました。

スタートアップコードの見直し

Microsoft.Extensions.HostingEnv.IsDevelopment()で拡張メソッドを使っていますが、using参照が足りていなかったので追加しました。
RouteConfigBundleConfigがApplication_Startで使われているので移植されています。
スタートアップコードはこのような形でStartup.csへ移植されます。
ただし、デフォルトでASP.NETの該当コード(RouteTable, BundleTable)は無効化されているので、この部分も併せて無効化します。

Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

//add
using Microsoft.Extensions.Hosting;

namespace aspdotnetwebform20211222
{
    public class Startup
    {
        ...

        public void Configure(IApplicationBuilder app)
        {
            app.UseStaticFiles();
            app.UseRouting();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapBlazorHub();
                endpoints.MapFallbackToPage("/_Host");
            });

            if (Env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            //// アプリケーションのスタートアップで実行するコードです
            //// The following lines were extracted from Application_Start
            //RouteConfig.RegisterRoutes(RouteTable.Routes);
            //BundleConfig.RegisterBundles(BundleTable.Bundles);

        }

        ...

    }
}

ちなみにBlazor ServerのStartup.csの構造は以下を参考に確認しました。

共通レイアウトの名前空間変更

ASP.NETのデフォルトページではメニューなどを含む共通ページと個別のページが用意されています。
各ページはRazorファイルへコンバートされていますが、テンプレート部分も同じようにLayoutComponentBaseを継承する形でレイアウトファイルに移植されます。

さて、Razorファイル上の@namespaceがなぜかReplace_this_with_code_behind_namespaceになっていると思うので、コードビハインドを確認して、名前空間を修正しましょう。
ここを修正しないと、個別のページからレイアウトファイルが参照出来ません。

Site.razor

@namespace Replace_this_with_code_behind_namespace
@inherits LayoutComponentBase
<!-- Conversion of AutoEventWireup attribute (value: "true") for Master directive not currently supported -->
<!-- Conversion of Inherits attribute (value: "aspdotnetwebform20211222.SiteMaster") for Master directive not currently supported -->

<!DOCTYPE html>

    <form runat="server">
        @* The following tag is not supported: <asp:ScriptManager runat="server"> *@

        @* </asp:ScriptManager> *@

        <div class="navbar navbar-inverse navbar-fixed-top">
            <div class="container">
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" runat="server" href="~/">アプリケーション名</a>
                </div>
                <div class="navbar-collapse collapse">
                    <ul class="nav navbar-nav">
                        <li><a runat="server" href="~/">ホーム</a></li>
                        <li><a runat="server" href="~/About">詳細</a></li>
                        <li><a runat="server" href="~/Contact">問い合わせ</a></li>
                    </ul>
                </div>
            </div>
        </div>
        <div class="container body-content">
            @Body
            <hr>
            <footer>
                <p>&copy; @(DateTime.Now.Year) - マイ ASP.NET アプリケーション</p>
            </footer>
        </div>

    </form>

Blazorコンポーネント追加

ASP.NET Web Formsのコンポーネントを同名でBlazorコンポーネントとして提供するBlazorWebFormsComponentsというパッケージがあります。
Porting Assistant for .NETではASPX, ASCXファイルはそのままRazorファイルへ変換されており、同じコントロール名を利用出来るように、デフォルトでこのパッケージを参照しているようです。

NuGetパッケージへの追加は自分で行う必要があります。

コンポーネント提供元がまとめている以下の移行ステップのページは是非目を通しておきたいところです。
マイクロソフトの移行ガイドラインについて後述しますが、こちらのページでは移行に向けて必要なより実践的なTipsも含まれています。

個別の修正対応

コードビハインドに依存している部分の個別の修正も必要になる場合があります。
今回はASP.NET Web Formsの既定のテンプレートをそのまま移植したのですが、コードビハインドのTitleプロパティをビューテンプレート側で参照していて、そこが解決できていませんでした。

今回はコードビハインドにプロパティを追加しました。

@page "/About"
@layout Site
@inherits aspdotnetwebform20211222.About
<!-- Conversion of Title attribute (value: "About") for Page directive not currently supported -->
<!-- Conversion of AutoEventWireup attribute (value: "true") for Page directive not currently supported -->


    <h2>@(Title).</h2>
    <h3>Your application description page.</h3>
    <p>Use this area to provide additional information.</p>

実行

既定のWebアプリケーションであれば、ここまででBlazor Server化が完了しました。
後は実行してテストしながら修正していきましょう。

移行後のデフォルトだと、Startup.csでデフォルトテンプレートで設定されるHTTPSリダイレクトなどのコードが含まれていないので、必要に応じてそのあたりも追加していきましょう。

さいごに

そもそもASP.NET Web Formsアプリケーションを移行すべきなのかというところだけ触れておきたいと思います。
マイクロソフトでもBlazorへの移行ガイドラインが用意されていますが、その中で以下のように触れられています。

Why should a working app be migrated to Blazor? Many times, there's no need. ASP.NET Web Forms will continue to be supported for many years.

ASP.NET Web Formsは今後もサポートされ続ける予定なので無理に移行する必要はないよと。
アプリケーションをLinuxやmacOSへ移行したい場合や、WebAssembllyの機能を使いたい場合などは移行を検討もアリでしょう。くらいの温度感でしょうか。

このあたりはクラウドへ移行すること自体を目的にするのではなくて何のために移行するのですか?必要なのですか?というのとよく似ていますね。

もし移行を検討される場合であれば本ツールは部分的なサポートしてくれる有効なツールなのではないかなと思います。
re:Invent 2021のセッションで.NETなどエンタープライズ寄りなアプリケーションのモダナイゼーションを意識している感覚は受けましたが、AWSもBlazorに注目してこういった取り組みをしている点は興味深いですね。