.NET Core 2.0のリリースによって、Microsoftは、2016年にリリースした汎用目的でモジュール構造、プラットフォーム不問のオープンソースプラットフォームの次期メジャーバージョンを手にしました。.NET Coreは、現行の.NET Frameworkで利用可能なAPIの多くを備えるように設計されています。元々はASP.NETソリューションの次世代版を実現するために開発されたものですが、現在ではIoTやクラウド、次世代モバイルソリューションなど、まったく違うシナリオを担うための基盤的な存在となっています。.NET Coreを取り上げたこのシリーズの第2回では、.NET Coreのさらなるメリットと、既存の.NET開発者のみならず、堅牢性、パフォーマンス、経済性を兼ね備えたソリューションを市場に投入する必要のあるすべての技術者にとっての価値を探ります。
本記事は".NET Core"シリーズの一部です。新たな記事発行の通知はRSS経由で受け取ることができます。
私は最近,古いフル.NETフレームワークからの切り替えを躊躇している,あるいはできない人たちから,.NET Coreに移行することのメリットは何か,という質問を受けています。そこで回答として,パフォーマンスの向上,csprojファイルフォーマットの改善,ASP.NET Coreのテスト性向上,クロスプラットフォームであることについて説明したいと思います。
いくつかのOSSツール(今回のプロジェクトではMarten,StructureMap,Albaが例として参照されています)の作者である私個人にとっての最も大きなメリットは,おそらくdotnet cliの出現でしょう。新しくなった.NET SDK csprojファイルフォーマットと関連付けて使用することで,dotnet cliツールは,私個人のプロジェクト生成とメンテナンスを非常に簡単なものにしてくれました。ビルドスクリプト内でのテスト実行や,Nugetパッケージの利用と公開が簡単になるだけでなく,cliの拡張メカニズムは,Nugetパッケージ経由で配布されるカスタム実行可能ファイルを自動ビルドに組み込むのに最適なのです。
dotnet cliを使い始めるには,まず開発マシンに.NET SDKをインストールします。それが終わったら、覚えておくと便利なことがいくつかあります。
- "dotnet"ツールはPATH内にグローバルにインストールされ,コマンドラインプロンプトでどこからでも利用可能です。
- dotnet cliは,オプションフラグとして普通の形式の"--word [value]"または省略形式の"-w [value]"を使用するという,Linuxスタイルのコマンド構文を採用しています。GitやNode.jsのコマンドラインツールを使っていれば,dotnet cliは使いやすいと感じるでしょう。
- "dotnet --help"で,インストールされているコマンドと,基本的な構文の使用法がリストされます。
- "dotnet --info"で,使用しているdotnet cliのバージョンが分かります。ローカルでは正しく動作するものがビルドサーバで失敗する場合や,その逆の場合のトラブルシュートのために,継続的インテグレーションのビルド内でこのコマンドをコールしておくとよいでしょう。
- この記事では.NET Coreについて取り上げていますが,新しいSDKプロジェクトフォーマットやdotnet cliは,以前のバージョンのフル.NET Frameworkでも使用することができます。
コマンドラインからのHello World
dotnet cliのハイライトのいくつかをガイドツアー形式で紹介するため,簡単な"Hello, World" ASP.NET Coreアプリケーションを開発するものとしましょう。ただし,それだけではつまらないので,少し工夫をしてみます。
- 作成するWebサービスの自動テストを実施するための別プロジェクトを用意します。
- 最近の流行りなので(そしてdotnet cliの機能をより多く紹介するために),開発するサービスはDockerコンテナを介してデプロイします。
- そしてもちろん,dotnet cliを可能な限り使用します。
完成したコードをご覧になりたい場合は,こちらのGitHubリポジトリを参照してください。
最初は"DotNetCliArticle"という空のディレクトリを作って,そのディレクトリで好みのコマンドラインツールをオープンすることから始めます。まず"dotnet new"コマンドを使って,ソリューションファイルと新しいプロジェクトを生成します。.NET SDKには,一般的なプロジェクトタイプやファイルを生成するための共通テンプレートが同梱されていますが,他のテンプレートをアドオンとして使用することも可能です(後のセクションで詳しく説明します)。自身のマシンで使用可能なテンプレートを調べる場合は,コマンド dotnet new --help を使用すれば,このようなアウトプットを得ることができます。
上記で分かるように,利用可能なテンプレートのひとつとして,空のソリューションファイルのための"sln"があります。今回はこのテンプレートを使用するので,最初にdotnet new sln
と入力します。そうすると次のようなアウトプットが得られます。
The template "Solution File" was created successfully.
デフォルトでは、このコマンドはソリューションファイルに対して、その置かれているディレクトリに従った名称を付けます。私はルートディレクトリを"DotNetCliArticle"にしたので、生成されるソリューションファイルは"DotNetCliAraticle.sln"です。
次に、このコマンドで"Hello, World"に実際のプロジェクトを追加しましょう。
dotnet new webapi --output HeyWorld
上のコマンドは、"output"フラグで指定した"HeyWorld"ディレクトリで"webapi"テンプレートを実行するものです。このテンプレートは、ヘッドレスAPI用に簡略化したMVC Coreプロジェクト構造を生成します。 ここでもデフォルトでは、ディレクトリ名に従ってプロジェクトファイル名が付けられます。今回はディレクトリに,"HeyWorld.csproj"というファイルと、最小限のASP.NET MVC Core APIプロジェクトの基本ファイルが生成されます。さらに、新しいプロジェクトの開発を開始するために必要なASP.NET Coreに対する、必要なすべてのNugetリファレンスも設定されます。
私はこれを小さなGitリポジトリ内に構築したので、git add
で新たなファイルを追加した後で、git status
を使って追加されたものを確認しました。
new file: HeyWorld/Controllers/ValuesController.cs
new file: HeyWorld/HeyWorld.csproj
new file: HeyWorld/Program.cs
new file: HeyWorld/Startup.cs
new file: HeyWorld/appsettings.Development.json
new file: HeyWorld/appsettings.json
次に、下のような"dotnet sln"コマンドを使って、空のソリューションファイルに新しいプロジェクトを追加します。
dotnet sln DotNetCliArticle.sln add HeyWorld/HeyWorld.csproj
ここまではVisual Studio .NET(私の場合はJetBrains Rider)を開く必要もなく、新しいASP.NET Core APIサービスのshellで行うことができました。もう少し進めて、実際にコードを書く前にプロジェクトのテストを始められるように、次のようなコマンドを実行します。
dotnet new xunit --output HeyWorld.Tests
dotnet sln DotNetCliArticle.sln add HeyWorld.Tests/HeyWorld.Tests.csproj
上のコマンドは、テストライブラリとしてxUnit.NETを使ったプロジェクトを新たに生成して、ソリューションファイルに追加します。テストプロジェクトには"HeyWorld"プロジェクトへのプロジェクト参照が必要ですが、便利な"dotnet add"ツールを使って次のように追加することができます。
dotnet add HeyWorld.Tests/HeyWorld.Tests.csproj reference HeyWorld/HeyWorld.csproj
ソリューションを開く前に,テストプロジェクトで使用したいNugetの参照が他にいくつかあります。Shouldlyは私がいつも使っているアサーションツールなので,次のコマンドラインを実行して最新バージョンの参照を加えておきましょう。
dotnet add HeyWorld.Tests/HeyWorld.Tests.csproj package Shouldly
実行すると、次のようなコマンドラインアウトプットが得られます。
info : Adding PackageReference for package 'Shouldly' into project 'HeyWorld.Tests/HeyWorld.Tests.csproj'.
log : Restoring packages for /Users/jeremydmiller/code/DotNetCliArticle/HeyWorld.Tests/HeyWorld.Tests.csproj...
info : GET https://api.nuget.org/v3-flatcontainer/shouldly/index.json
info : OK https://api.nuget.org/v3-flatcontainer/shouldly/index.json 109ms
info : Package 'Shouldly' is compatible with all the specified frameworks in project 'HeyWorld.Tests/HeyWorld.Tests.csproj'.
info : PackageReference for package 'Shouldly' version '3.0.0' added to file '/Users/jeremydmiller/code/DotNetCliArticle/HeyWorld.Tests/HeyWorld.Tests.csproj'.
次に,少なくともあとひとつ,AlbaというテストプロジェクトへのNuget参照を追加します。新しいWebアプリケーションのHTTPコントラクトテストの記述に,AspNetCore2も使用します。
dotnet add HeyWorld.Tests/HeyWorld.Tests.csproj package Alba.AspNetCore2
ここまで完了したならば,コードを書く前に,ちょっとしたチェックをしましょう。コマンドラインで,ソリューションのすべてのプロジェクトをビルドする次のコマンドを実行して,すべてがうまくコンパイルできることを確認します。
dotnet build DotNetCliArticle.sln
おや,コンパイルできませんでした。Alba.AspNetCore2と,HeyWorldプロジェクト内のASP.NET CoreのNuget参照バージョンとの間に,依存関係のダイヤモンド競合があるためです。ですが,心配することはありません。次のように,Microsoft.AspNetCore.All
Nugetのバージョン依存関係を修正することで,簡単に対処できます。
dotnet add HeyWorld.Tests/HeyWorld.Tests.csproj package Microsoft.AspNetCore.All --version 2.1.2
上の例では,"--version"フラグを"2.1.2"の値で使用することで,Nugetの提供している最新バージョンではなく,指定したバージョンを参照するように修正しています。
Nugetの依存性問題がすべて解決したことを再確認するためには,次のコマンドを使用すれば,すべてを再コンパイルするよりも早くチェックすることができます。
dotnet clean && dotnet restore DotNetCliArticle.sln
私は.NET開発を長く経験しているのですが,テンポラリの/objや/binフォルダにファイルが残ることがいつも気になるので,参照を変えたい時には必ず,Visual Studio.NETの"Clean Solution"コマンドを使って,後にファイルが残らないようにしています。"dotnet clean"コマンドは,これとまったく同じことをコマンドラインから行うものです。
同じように,"dotnet restore"コマンドは,指定したソリューションファイルの既知のNuget依存関係をすべて解決しようと試みます。ここでは"dotnet restore"を使うことで,全体をコンパイルしなくても,潜在的な競合や不足しているNuget参照をすばやく見つけることができます。ですから私は,いつも仕事ではこのコマンドを使用しています。最新バージョンのdotnet cliでは,最初にNugetが必要となる"dotnet build/test/pack/etc"コマンドでは,Nuget解決が自動的に実行されます。(フラグでこの動作をオーバーライドすることもできます)。
"dotnet restore DotNetCliArticle.sln"の実行がエラーなしで終了したので,いよいよコードを書く段階になりました。お好きなC#エディタを開いて,HeyWorld.Testsプロジェクトに,HeyWorldアプリケーションの"GET: /"ルートから希望する動作を記述した,単純なHTTPコントラクトテストを含むコードファイルを追加してください。
using System.Threading.Tasks;
using Alba;
using Xunit;
namespace HeyWorld.Tests
{
public class verify_the_endpoint
{
[Fact]
public async Task check_it_out()
{
using (var system = SystemUnderTest.ForStartup<Startup>())
{
await system.Scenario(s =>
{
s.Get.Url("/");
s.ContentShouldBe("Hey, world.");
s.ContentTypeShouldBe("text/plain; charset=utf-8");
});
}
}
}
}
完成したファイルは、verify_the_endpoints.cs.
などの適当な名前でHeyWorld.Tests
ディレクトリに保存します。
Albaの構造について詳しく説明はしませんが、ここでは新しいHeyWorldアプリケーションのホームルートが"Hety, World"という文字列を出力しなければならない、という点のみ述べておきます。HeyWorldアプリケーションにはまだ何もコーディングしていませんが、まずはこのテストを実行して、正しく接続されていること、"まっとうな理由"で正しく失敗することを確認しておきましょう。
コマンドラインに戻って、次のコマンドでテストプロジェクトの全テストを実行することができます。
dotnet test HeyWorld.Tests/HeyWorld.Tests.csproj
実行すると、実際に何もコーディングをしていないのでテストが失敗し、このように表示されます。
Build started, please wait...
Build completed.
Test run for /Users/jeremydmiller/code/DotNetCliArticle/HeyWorld.Tests/bin/Debug/netcoreapp2.1/HeyWorld.Tests.dll(.NETCoreApp,Version=v2.1)
Microsoft (R) Test Execution Command Line Tool Version 15.7.0
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
[xUnit.net 00:00:01.8266290] HeyWorld.Tests.verify_the_endpoint.check_it_out [FAIL]
Failed HeyWorld.Tests.verify_the_endpoint.check_it_out
Error Message:
Alba.ScenarioAssertionException : Expected status code 200, but was 404
Expected the content to be 'Hey, world.'
Expected a single header value of 'content-type'='text/plain', but no values were found on the response
Stack Trace:
at Alba.Scenario.RunAssertions()
at Alba.SystemUnderTestExtensions.Scenario(ISystemUnderTest system, Action`1 configure)
at Alba.SystemUnderTestExtensions.Scenario(ISystemUnderTest system, Action`1 configure)
at HeyWorld.Tests.verify_the_endpoint.check_it_out() in /Users/jeremydmiller/code/DotNetCliArticle/HeyWorld.Tests/verify_the_endpoint.cs:line 14
--- End of stack trace from previous location where exception was thrown ---
Total tests: 1. Passed: 0. Failed: 1. Skipped: 0.
Test Run Failed.
Test execution time: 2.5446 Seconds
アウトプットからは,ひとつのテストが実行されて,それが失敗しているのが分かります。テストが失敗した理由について情報を提供する,標準的なxUnitアウトプットも見えます。ここで重要なのは,"dotnet test"コマンドが,すべてのテストがパスした場合には終了コードとして成功を示すゼロを,失敗したテストのある場合は失敗を占める非ゼロの終了コードを返すことです。このことは,継続的インテグレーション(CI)スクリプティングで重要になります。ほとんどのCIツールが,ビルドの失敗を知るためのコマンドの終了コードを使用しているからです。
このテストが失敗したのには"まっとうな理由"があると判断できます。つまり,テストハーネスは実際のアプリケーションのブートストラップに成功して,コーディングをまだ実施していないので期待通り404を応答した,ということです。次に,期待される動作をするMVC Coreエンドポイントを実装しましょう。
public class HomeController : Controller
{
[HttpGet("/")]
public string SayHey()
{
return "Hey, world!";
}
}
(上記のコードは,追加クラスとしてHeyWorld\startup.csファイルに記述する必要があります。)
もう一度コマンドラインに戻って,先程の"dotnet test HeyWorld.Tests/HeyWorld.Tests.csproj
"コマンドを実行してみましょう。次のような結果が得られると思います。
Build started, please wait...
Build completed.
Test run for /Users/jeremydmiller/code/DotNetCliArticle/HeyWorld.Tests/bin/Debug/netcoreapp2.1/HeyWorld.Tests.dll(.NETCoreApp,Version=v2.1)
Microsoft (R) Test Execution Command Line Tool Version 15.7.0
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.
Test execution time: 2.4565 Seconds
テストがパスしたので,次は実際のアプリケーションを実行しましょう。"dotnet new webapi"テンプレートでは,HTTPリクエストの処理にインプロセスのKestrel Webサーバを使用しているので,新しいHeyWorldアプリケーションの実行に必要なのは,次のコマンドをコマンドラインからローンチすることだけです。
dotnet run --project HeyWorld/HeyWorld.csproj
コマンドを実行すると,次のようなアウトプットが得られるはずです。
Using launch settings from HeyWorld/Properties/launchSettings.json...
: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using '/Users/jeremydmiller/.aspnet/DataProtection-Keys' as key repository; keys will not be encrypted at rest.
Hosting environment: Development
Content root path: /Users/jeremydmiller/code/DotNetCliArticle/HeyWorld
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
実行中の完成したアプリケーションをテストするには,ブラウザで次のようにナビゲートします。
HTTPSのセットアップについては,この記事では扱いません。
繰り返しになりますが,紹介したすべてのコマンドは,ソリューションルートフォルダにカレントディレクトリをセットした上で実行されているものと仮定していることに注意してください。カレントディレクトリがプロジェクトディレクトリで,ディレクトリ内に".csproj"ファイルがひとつしかない場合は,"dotnet run"で実行できます。
Web APIアプリケーションをテストしたので,次のステップに進んで,HeyWorldをDockerイメージに収めてみましょう。.NET CoreアプリケーションをDocker化する標準テンプレートを使って,次の内容を含むDockerファイルをHeyWorldプロジェクトに追加します。
FROM microsoft/dotnet:sdk AS build-env
WORKDIR /app
# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore
# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out
# Build runtime image
FROM microsoft/dotnet:aspnetcore-runtime
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "HeyWorld.dll"]
(上記のテキストは,プロジェクトディレクトリ内のDockerfile,この場合はHeyWorld/Dockerfileというテキストファイルに保存する必要があります。)
この記事はdotnet cliに関するものなので,Dockerfile内の2つの用法に焦点を絞りたいと思います。
- “dotnet restore" -- すでに説明したとおり,このコマンドは,アプリケーションのNuget依存関係を解決するものです。
- "dotnet publish -c Release -o out" -- "dotnet publish"コマンドは,指定されたプロジェクトをビルドした上で,アプリケーションを構成するすべてのファイルを指定された場所にコピーするものです。この場合の"dotnet publish"は,コンパイルしたHeyWorld自体のアセンブリ,Nuget依存関係から参照されるすべてのアセンブリ,構成ファイル,その他のcsprojファイルから参照されるファイルをコピーします。
ここで示した使い方では,"-c Release"フラグを使うことで,"dotnet publish"に"Release"コンフィギュレーションでコンパイルすることを明示的に指示しなくてはならない点に注意してください。コードをコンパイルするdotnet cliコマンド("build","publish","pack"など)はいずれも,デフォルトでは"Debug"コンフィギュレーションを使用したビルドアセンブリになります。この動作に注意して,実使用を目的としたNugetあるいはアプリケーションを公開する場合は,"-c Release"あるいは"--configuration Release"を忘れずに指定してください。警告しておきます。
一連の作業を完成させるために,以下のコマンドを使って,私たちのちっぽけなHeyWorldアプリケーションのビルドとDocker経由でのデプロイを行います。
docker build -t heyworld .
docker run -d -p 8080:80 --name myapp heyworld
最初のコマンドは"heyworld"というアプリケーションをビルドして,Dockerイメージをローカルに公開します。2つめのコマンドが実際に,アプリケーションを"myapp"という名前のDockerコンテナとして実行するものです。結果はブラウザで,"http://localhost:8080"を開けば確認できます。
まとめ
dotnet cliを使用することで,.NETプロジェクトのビルドの自動化とスクリプティングが簡単になります。特に10年ほど前の.NETの最新版と比べれば,その差は歴然です。多くの場合において,タスクベースのビルドスクリプトツール(Cake,Fake,Rake,Psakeなど)の代わりに,dotnet cliを使用した単純なシェルスクリプトを使うことが可能になります。さらに,拡張性を備えたdotnet cliモデルにより,.NETで記述されてNugetで配布されている外部のコマンドラインアプリケーションを,自分の自動ビルドに組み込むことも簡単です。
著者について
Jeremy Miller氏がミズーリ州の農村部で育った頃,周りにはShade Tree Mechanic(木陰の整備士)という"特別な"種類の人たちがいました。とても評判がよい,とは言い難い人たちでしたが,機械に関する問題を解決する特技と,何でもいじくり回すという恐れ知らずの大胆さを持っていました。Shade Tree Mechanicは,薄汚れたガラクタだらけの作業場にあるボロ車の中で,ブロックに乗せられた車の下から出ている足で見つけることができます。周りに捨てられているボロ車はごみではなく,部品取りのためのものです。部品を取って調整したり改造したりすることで,ニーズに対応する,クリエイティブなソリューションを生み出すのです。評判はともかくとして,彼らはさまざまな修理の方法を知っています。Miller氏は(機械工学の学位を持っているにも関わらず)特別な整備技術を持ってはいませんが,自分自身をこのShade Tree Mechanicと同じような開発者だと思っています。氏のハードディスクは,放棄されたオープンソースプロジェクトの残骸でいっぱいなのです。
.NET Core 2.0のリリースによって、Microsoftは、2016年にリリースした汎用目的でモジュール構造、プラットフォーム不問のオープンソースプラットフォームの次期メジャーバージョンを手にしました。.NET Coreは、現行の.NET Frameworkで利用可能なAPIの多くを備えるように設計されています。元々はASP.NETソリューションの次世代版を実現するために開発されたものですが、現在ではIoTやクラウド、次世代モバイルソリューションなど、まったく違うシナリオを担うための基盤的な存在となっています。.NET Coreを取り上げたこのシリーズの第2回では、.NET Coreのさらなるメリットと、既存の.NET開発者のみならず、堅牢性、パフォーマンス、経済性を兼ね備えたソリューションを市場に投入する必要のあるすべての技術者にとっての価値を探ります。
本記事は".NET Core"シリーズの一部です。新たな記事発行の通知はRSS経由で受け取ることができます。