.NET 5の重大な変更(Breaking Changes)シリーズのパート3では、ASP.NET Coreを取り上げる。前回までの記事では、Base Class Libraryと歴史的テクノロジをレポートした。
シリアライゼーション
引用符で囲まれた文字列が、通常のJSONの数値と同じように、数値プロパティとしてデシリアライズされるようになった。従来はJsonException
がスローされていた。
パッケージの更新
さまざまなパッケージの名称が変更されたり、リプレースされたりしている。以下のリストは、おもなパッケージとそのリプレースについてまとめたものだ。
AzureAD.UI => Microsoft.Identity.Web
AzureADB2C.UI APIs => Microsoft.Identity.Web
Microsoft.AspNetCore.DataProtection.AzureKeyVault => Azure.Extensions.AspNetCore.DataProtection.Keys
Microsoft.AspNetCore.DataProtection.AzureStorage => Azure.Extensions.AspNetCore.DataProtection.Blobs
Microsoft.Extensions.Configuration.AzureKeyVault => Azure.Extensions.AspNetCore.Configuration.Secrets
Blazorのパフォーマンス向上
重要でない空白が、.razor
ファイルのコンパイル時に取り除かれるようになった。これにより、メモリとネットワーク帯域を消費する何百という空白ノードが、レンダリングに影響を与えることなく容易に削除できる。Microsoftのテストによると、空白ノードはベンチマークにおけるレンダリング時間の最大40パーセントを占めていた。
RenderTreeFrame
構造体では、さまざまな読み取り専用の公開フィールドを読み取り専用の公開プロパティに置き換えることで、"Blazorコンポーネントのレンダリングに大きく影響するパフォーマンス改善の実装"が可能になった。この変更は、バイナリレベルでは非互換だが、ソースレベルでは互換性が保たれている。つまり、コードを修正する必要はないが、新たなバージョンで動作させるためには再コンパイルが必要になる、ということだ。ほとんどの開発者にとって、これは問題ではないが、
.NET Core 3と.NET Core 5の両方をサポートするライブラリ開発者は、両方のバージョンを対象とした条件付きMicrosoft.AspNetCore.Componentsパッケージ参照の使用が必要になる。Microsoftは次のようなサンプルを提供している。
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.0.0" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="5.0.0-rc.1.*" Condition="'$(TargetFramework)' != 'netstandard2.0'" />
このことは、.NET Framework Design Guidelinesの順守が重要であり続ける理由を明確にしている。MicrosoftがField Designルールに従っていれば、互換性を損なう変更は必要なかったはずなのだ。
BlazorのIEおよびEdge Legacyサポート終了
.NET 5のBlazorは、旧式のブラウザでは使用できない付加的機能を必要としている。それにより、Internet Explorerは完全にサポート対象外となった。これまでは、IE 11でpolyfills.とasm.jsを経由すれば、BlazorベースのWebサイトにアクセスすることが可能だった。しかしながら、Daniel Roth氏曰く"問題が多く、エクスペリエンスもよくなかった"ため、Microsoftではasm.jsルートを廃止して、WebAssemblyに集中することにしたのだ。
Edge Legacyも同じく、.NET 5のBlazorではサポートされない。理由は特に述べられていないが、このブラウザは2021年3月9日以降サポートされない予定であるため、不要という判断によるものと思われる。Edge Legacyは2020年1月に、ChromiumベースバージョンのEdgeによって公式にリプレースされている。
HttpClientロギング
場合によっては、比較的小さな変更がプロジェクトに重大な影響を与えることもある。そのようなケースのひとつが、がlHttpClientFactoryの生成したHttpClientインスタンスによる整数ステータスコードの生成方法だ。.NET Coreでは次のように、ログにはHTTPステータスコード名が記載されていた。For example:
Received HTTP response after 56.0044ms — OK
End processing HTTP request after 70.0862ms OK
.NETの他部分との一貫性向上のため、これがHTTPステータスコード整数値を使うように変更された。
Received HTTP response after 56.0044ms — 200
End processing HTTP request after 70.0862ms — 200
"Elasticsearchのような構造化ログシステムによるクエリを困難にする"不一致の排除、というのがその理由だ。しかしながら、既存の監視ツールがステータスコード名を検索するように設計されている場合、この変更は警告処理を棄損することになる。
以前の動作が必要な場合について、Microsoftでは、4つのロギングクラスの.NET Core 3.1バージョンのソースコードを取得した上で、それらを自身のプロジェクトに含めるように推奨している。後方互換性の問題に対処するためにMicrosoftがオープンソースを使用するのは、今回が初めてかも知れない。
BadHttpRequestExceptionからBadHttpRequestExceptionへのリプレース
この一見奇妙な変更は、.NETにBadHttpRequestException
というクラスが3つあることが原因だ。フルネームはそれぞれ、Microsoft.AspNetCore.Server.Kestrel.BadHttpRequestException
、Microsoft.AspNetCore.Server.IIS.BadHttpRequestException
、Microsoft.AspNetCore.Http.BadHttpRequestException
である。これらはすべて同じことを意味しているので、冗長であると判断された。
今後はHTTPバージョンが正式に認められたクラスとなり、他の2つはobsoleteとしてマークされると同時に、一定の後方互換性を確保するためにHTTPバージョンを継承するようになる。
HttpSysによる証明書再ネゴシエーションの無効化
パフォーマンス向上とデッドロック回避、およびHTTP/2互換性に関する理由から、HttpSysが既定値としてクライアント証明書の再ネゴシエーションを行わないようになった。再有効化は可能だが、Microsoftは以下の理由から有効にしないように推奨している。
- TLSの機能であり、HTTPの機能ではない。
- ネゴシエーションはコネクションの開始時、HTTPデータの送信以前に実施されるべきである。コネクション開始時には、SNI(Server Name Indication)のみが既知である。クライアントおよびサーバの証明書はコネクション上の最初のリクエストに先立ってネゴシエーションされるため、一般論としてリクエストで再ネゴシエーションを行うことはできない。
- HTTP/1.1では、リクエストのボディがTCPウィンドウを埋め尽くしている場合、再ネゴシエーションパケットを受信することができず、デッドロックを引き起こす可能性がある。
- HTTP/2では明示的に再ネゴシエーションを禁じている。
- TLS 1.3では再ネゴシエーションのサポートが削除されている。
Kestrel: TLSプロトコルバージョンの既定値の変更
Kestrelのサポート対象TLSプロトコルが、ハードコードされたリストから、オペレーティングシステムに従うように変更された。これにより、既存プロトコルに問題が発見された場合に、OSを介した無効化やパッチが可能になり、アプリケーションを変更する必要はなくなった。Windows上で現在サポートされているプロトコルの一覧については、資料"Ensuring support for TLS 1.2 across deployed operating systems"を参照して頂きたい。
この動作は、Kestrelの起動オプションHttpsOptions.SslProtocols
の値の設定によって、明示的にオーバーライドすることが可能だ。
Pubternal APIの削除
"pubternal"という用語を知らなくても心配はない、あなただけではないからだ。この聞きなれない単語は、publicとマークされているにも関わらず、ライブラリの内部使用であることを暗示するネームスペース内にあるAPIを指す。ASP.NET Coreには、この例が2つある。
Microsoft.Extensions.Localization.Internal.AssemblyWrapper
Microsoft.Extensions.Localization.Internal.IResourceStringProvider
これらのクラスは元々、Microsoftが"チームの内部テスト用の拡張ポイント"として使用する目的でpublicとマークされていたものだ。.NETにはInternalsVisibleTo属性など、これを扱うもっとよい方法がすでに提供されているので、このようなことは行うべきではない。そこで、Microsoft以外の開発者がこれらクラスを使用することで生じる将来的な問題を回避するため、internalとマークされることになった。
MVC: ObjectmodelvalidatorからのValidationvisitor.Validateの新しいオーバーロード呼び出し
通常であれば、クラスに新たなメソッドを追加しても、互換性が失われることはない。しかしValidationVisitor.Validateの場合、事態はもう少し複雑だ。
このメソッドのオリジナルのシグネチャは次のようなものだった。
public virtual bool Validate(ModelMetadata metadata, string key, object model, bool alwaysValidateAtTopLevel)
仮想メソッドなので、独自のバリデーションロジックを実行するためにオーバーロードすることが可能だ。ここで実装したロジックが、MVCのObjectModelValidatorによって呼び出されることになる。
.NET 5では、新たな仮想オーバーロードが導入されて、オリジナルはそれをコールするようにリダイレクトされている。
public virtual bool Validate(ModelMetadata metadata, string key, object model, bool alwaysValidateAtTopLevel)
=> Validate(metadata, key, model, alwaysValidateAtTopLevel, container: null);
public virtual bool Validate(ModelMetadata metadata, string key, object model, bool alwaysValidateAtTopLevel, object container)
これらが仮想メソッドでなければ、何も問題はなかった。開発者はただ、新しいオーバーロードがコールされていることを知らずに済んだのだが、残念ながらそうではなかった。
オリジナルのValidate
がオーバーライドされている場合、.NET 5では、そのオーバーライドは無視されるようになる。コードのコンパイルは引き続き可能だが、ObjectModelValidator
が新しいオーバーロードの方を呼び出すため、何の理由もなく、ただ単に動作が不正になるのだ。
問題を緩和する要素として考えられるのは、FluentValidationを除けば、このメソッドが実際にオーバーライドされていることはなさそうだ、という点である。この変更の影響に関するディスカッションスレッドのいずれに対しても、コミュニティからの応答は得られていない。これはつまり、このメソッドを仮想にする必要が最初からなかった、ということでもある。
Cookie名エンコードの削除
HTTP標準では、Cookie名はASCII文字のサブセットに制限されているが、開発者の便宜のため、ASP.NET Coreを含む多くのフレームワークでは、単純なエンコードを行うことで、他の文字を含むことを可能にしている。この機能がリスクの原因となるため削除された。
このエンコード/デコードによって、"__Host-"などの予約済プレフィックスを"__%48ost-"のようなエンコード値にスプーフ(なりすまし)することで、攻撃者がcookie prefixesと呼ばれるセキュリティ機能をバイパスすることが可能になる、という問題が、複数のWebフレームワークで見つかっているためだ。この攻撃にはXSS脆弱性など、WebサイトになりすましのCookieを挿入するためのエクスプロイトがもうひとつ必要になる。これらのプレフィックスはASP.NET Core、Microsoft.Owinライブラリおよびテンプレートでは、既定値としては使用されない。
.NET 5ではCookie名のエンコード/デコードは行われなくなった。名前の中に非ASCII文字がある場合には例外がスローされる。なお、Cookieの値については、通常通りのエンコード/デコードが今後も行われる。
SignalR: MessagePack Hub ProtocolがMessagePackSerializerOptionsを使用
これまでSignalRでは、メッセージシリアライゼーション処理にMessagePack 1を使用していた。.NET 5では、それがMessagePack2にアップデートされている。MessgaePack 2では、IFormatterResolver
からMessagePackSerializerOpstions
への切り替えという大きな変更が行われているため、SignalRでもコンフィギュレーションの操作方法について同様な変更が必要になる。
この変更が影響するのは、MessagePackHubProtocolOptions
をSignalR用に変更している場合のみだ。
SignalRでエンドポイントルーティングが必要に
我々の知っているエンドポイントルーティングが現れたのはASP.NET Core 3以降である。"Understanding ASP.NET Core Endpoint Routing"と題した記事で、Areg Sarkissian氏が導入の理由について説明している。
エンドポイントルーティング以前のASP.NET Coreアプリケーションのルーティングレゾリューションは、ASP.NET Core MVCミドルウェア内の、HTTPリクエスト処理パイプラインの終端で行われていました。これはつまり、ミドルウェアパイプライン内でMVCミドルウェアより前にリクエストを処理するミドルウェアには、どのようなコントローラアクションが実行されたかというようなルート情報は提供されない、ということです。
このルート情報が特に有効なのは、例えばCORSや認証ミドルウェアなどです。認証プロセスのファクタのひとつとして、この情報を利用できるからです。
さらにエンドポイントルーティングは、ルートマッチングロジックをMVCミドルウェアから分離して、独自のミドルウェアに移動することも可能にします。これによってMVCミドルウェアは、エンドポイントルーティングミドルウェアによって解決されるコントローラアクションメソッドに対して、リクエストをディスパッチするという、自身の責務に集中することができます。
このようにして、SignalRにエンドポイントルーティングを使用する機能が追加されたのだが、その時点ではオプションという扱いだった。obsoleteとマークはされたものの、それ以前の独自ルーティングメカニズムが引き続き利用が可能だったのだ。.NET 5では、この古いメソッドは完全に取り除かれて、エンドポイントの使用が必須になる。
幸いなことに、コードの物理的な変更は極めてマイナーだ。
app.UseSignalR(routes =>
{
routes.MapHub<SomeHub>("/path");
});
これを次のように変更すればよい。
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<SomeHub>("/path");
});
静的ファイル: CSV Content Typeが標準準拠に
今日取り上げる最後の変更は、CSVファイルの標準コンプライアンスだ。奇妙なミスだが、ASP.NET Coreの前バージョンでは、静的なCSVファイルが"text/csv
"ではなく、"application/octet-stream
"として提供されている。これはブラウザによるファイル処理に影響する可能性があるため、開発者はFileExtensionContentTypeProvider
を使ってこの動作をオーバーライドしなければならなかった。
万が一、ASP.NET Core 5で以前の動作が必要になった場合は、FileExtensionContentTypeProvider
を使ってapplication/octet-stream
に戻すことが可能だ。
パート4では、GUIフレームワークのWPFとWindows Formsを取り上げる。