.NET Core 3 Previewを適用してみる

 .NET Core 3のプレビューが6まで進んでいる。このブログは.NET Coreで作っているので、3への移行準備としてPreviewバージョンで動かしてみた。


 移行のために従ったページは↓
https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-2.2&tabs=visual-studio

 移行のDiffは↓
https://github.com/hMatoba/tetsujin/pull/86/files
https://github.com/hMatoba/tetsujin/pull/87/files

 実際の手順としては、まず.NET Core 3.0をVisualStudioから使えるようにしなければならなかったので、VisualStudioの最新のプレビューチャンネルを入れた。そしてそのVisualStudioでソリューションを開き、各プロジェクトのターゲットフレームワークを.NET Core 3.0に上げた。
 あとはマイグレーションガイドに従ったが、一つ、漏れがあって、ローカルでDockerを使って動かしていても問題なかったが、k8s環境で下記のエラーが出た。
System.InvalidOperationException: No service for type 'Microsoft.AspNetCore.Mvc.Infrastructure.IActionResultExecutor`1[Microsoft.AspNetCore.Mvc.JsonResult]' has been registered.

 このエラーを繰り返され、Podを何度も停止させた。エラーから察するにJSONを使うためのサービスがなさそうだったので、マイグレーションガイドに従ってNewtonsoftJSONを呼んだ。
services.AddMvc()

.AddNewtonsoftJson();


 ターゲットフレームワークのバージョン変更、Startup.csの変更などを経て、いまのところ問題なく動作するようになった。
comment: 0

ASP.NET Core MVCのプロジェクトの.NET Coreを2.1 RCにアップグレードした

 このブログでバックエンドに使っている.NET Coreのバージョン2.1がRelease Candidateまで来た。ここらでアップグレード準備をしておくことにした。
 このブログのバックエンドになっているプロジェクトは、Dockerコンテナに入ったASP.NET Core MVC使用のアプリで、TravisCIにてコンテナのビルドとテストまで行われる構成になっている。


 やってみると、参考になるドキュメントさえあれば一時間足らずで終わるものだった。アップグレードのための変更点は、プレビューの時点のもので抑えておくことで問題なかった。
ASP.NET Core 2.1.0-preview2 now available


 具体的な変更としてまず最初にコミットへのリンクを置いておく。
https://github.com/hMatoba/tetsujin/commit/3bc013f237007c64008dc41adb81f14d1384792c


 ここから行った変更について書いていく。まず開発環境に.NET Core 2.1(RC)のSDKをインストールする。
https://www.microsoft.com/net/download/all

 つづいてVisual Studioで、プロジェクトエクスプローラで対象プロジェクトを右クリックして、プロパティからターゲットフレームワークを.NET Core 2.1に変更する。



 同じくプロジェクトエクスプローラで対象プロジェクトを右クリックして、「NuGetパッケージの管理」を開いて、プレリリースを含めるにチェックを入れて導入済みパッケージで必要なパッケージのアップグレードを施していく。

 C#コードをちょっとだけ変更。
Program.cs

public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}


 Startup.csの該当箇所を変更。
services.AddMvc();

  ↓
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);


 Dockerfileで使うコンテナを変更。これまではmicrosoft/aspnetcoreをつかっていたが、これは廃止されてmicrosoft/dotnetでタグ分けで配布されるようになるというハナシ。
https://blogs.msdn.microsoft.com/webdev/2018/04/12/asp-net-core-2-1-0-preview2-now-available/#user-content-deprecating-aspnetcore-and-aspnetcore-build-docker-images
Dockerfile

FROM microsoft/dotnet:2.1.0-rc1-aspnetcore-runtime


 さいごにTravisCIの設定YAML。使用するdotnet CLIを2.1対応のものに変更する。
.travis.yml

dotnet: 2.1.300


 いわゆるフレームワークのアップグレードを実行した。変更箇所が少なかったのがありがたいところ。一方で、動作を保証したいところには自動テストを組んでたことがやはりよかった。最低限の動作が壊れないことに加え、どこを変更すべきかという勘所をつけることができた。
comment: 0

WebアプリのテストでUIテストと統合テストを切り離そうとしたけど、ちょっと考えなければならなかったハナシ

 Webアプリのテストで、Seleniumを使ったUIテストを書いておいた。テスト内容として、フォームを埋めて、POSTでコントローラが意図通りに動いて、のぞんだレスポンスが返ってくるかまでテストしていた。UIテストと同時に統合テストの役割をになっていたので、役割をフォームのテスト程度に抑えて、別の統合テストを用意したかった。だからPython + requests( +lxml)で、直接RESTリクエストを投げてコントローラを動かすテストを書いてみた。
https://github.com/hMatoba/tetsujin/blob/master/tetsujin/PythonIntegtationTest/tests/master_test.py


 ただGETを投げてレスポンスを確認するようなテストもSeleniumでやっていたのだが、これをrequests + lxmlにしたところパフォーマンスは数十倍を超えたのでまあいい手ごたえ。ただSeleniumを使ったテストでも、一つのメソッドにつき一秒もかかっていないのでそんな神経質にならんでもいいかとも思う。
 考えなければならんかったのはPOSTを行うフォームのテスト。POSTする内容をrequestsで作る。それをPOSTで投げる。なのだが、フォームなのでもちろんCSRF対策が入っていて、これを考慮せんといかんかった。
 ASP.NET Core MVCでのCSRF対策は備え付けのものを使った。
Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core
CSRF対策のトークンを確認したところ、フォームには”__RequestVerificationToken”の名前で、クッキーには”.AspNetCore.Antiforgery.9TtSrW0hzOs”のキーで値が入っていた。だが、長さ190文字中、先頭30文字弱のみ一致しており、それ以降は異なっていた。これに関して自分で適当に作ってフォームとCookieにセットするのはやめておいたほうがよさそう。ここはSeleniumを使ってブラウザでフォームを動かしたほうが、自分でごにょごにょやるより手っ取り早いだろう。

 まとめ。WebアプリのUIテストと統合テストをわけたかったので、RESTリクエストを作ってコントローラを動かすテストを追加してみた。GETでレスポンス確認する場合は狙い通りだった。いっぽうでPOSTでCSRF対策が入っている場合、フォームやクッキーにその対策の対策を入れる必要に気づいた。そうすると、そこのあたりはSeleniumを使ってブラウザを動かすテストにしておくのが余計なことを考えずに済んで手っ取り早いだろうという知見を得た。
comment: 0

コントローラからOAuthの処理のべた書きを締め出して、Dependency Injectionで入れたらかなり納得する形になった

 ASP.NET Core MVCでOAuth(Githubの)を実装したのだが、実装を急いだので処理をコントローラにべた書き+グローバルな静的クラスに任せていた。それをリファクタリングして、なおかつOAuthは他でも使いまわせるようにしたかった。できればOAuthの処理の普遍な部分は抽象クラスにして、個別のWebアプリで実装すべき部分は派生クラスで実装するような。
 そんなクラスにOAuthを切り出して、それをコントローラにDependency Injectionしたらいい形になったなぁと思ったハナシ。

 まずOAuthの、Webアプリサーバサイドでの処理。
事前準備: OAuthを使わせてくれるサイトに必要情報を登録し、ClientIDとClientSecretをもらっておく。
以下OAuth処理
OAuthを使わせてくれるサイトへのリンクをクライアントに渡す

クライアントはリンクを踏む。一旦自サイトから外に出るが、リダイレクトで自分のサイトへ戻ってくる。戻ってくるときにコードを持っている

自分のサイトのサーバで、コードを使って、OAuthをやらせてくれるサイトからトークンをもらう

取得したトークンでOAuthをやらせてくれるサイトからユーザ情報をもらう

あとはユーザ情報を自サイトで判別

 問題のOAuth処理べた書きコントローラ。
[Route("OAuth")]

public async Task<IActionResult> OAuthAsync()
{
var clientId = GithubOAuth.ClientId;
var clientSecret = GithubOAuth.ClientSecret;


if (!Request.Query.ContainsKey("code"))
{
ViewData["OAuthLink"] = $"https://github.com/login/oauth/authorize?client_id={clientId}";
return View("OAuthAsync");
}

var code = Request.Query["code"];

var token = await GetAcessTokenAsync(clientId, clientSecret, code);

// 取得したトークンを使ってGithubにユーザ情報を要求する
var id = await GetUserId(token);
var loginSuccess = GithubOAuth.Login(id, Response.Cookies);

return Redirect(loginSuccess ? "/Master/" : "/Auth/OAuth");
}

private async Task<string> GetAcessTokenAsync(string clientId, string clientSecret, string code)
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

// 持って帰ってきた認証コードを使ってトークンを取得する
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "client_id", clientId },
{ "client_secret", clientSecret },
{ "code", code },
});
var response = await httpClient.PostAsync("https://github.com/login/oauth/access_token", content);
var responseBody = await response.Content.ReadAsStringAsync();
var jsonObj = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseBody);
var token = jsonObj["access_token"];

return token;
}

private async Task<string> GetUserId(string token)
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

var uri = $"https://api.github.com/user?access_token={token}";
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Zenigata");
var response = await httpClient.GetAsync(uri);
var responseBody = await response.Content.ReadAsStringAsync();
var userInfo = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseBody);
var id = userInfo["id"];

return id;
}


 ビフォーアフターのアフターなコントローラ。OAuth処理を、専用のオブジェクト(インスタンス)に頼っている。専用のオブジェクトは、Dependency Injectionでコントローラにて使えるようになっている。
public class AuthorizationController : Controller

{
private readonly GithubOAuth _githubOAuth;

public AuthorizationController(GithubOAuth githubOAuth)
{
this._githubOAuth = githubOAuth;
}

[Route("OAuth")]
public async Task<IActionResult> OAuthAsync()
{
if (!Request.Query.ContainsKey("code"))
{
ViewData["OAuthLink"] = this._githubOAuth.GetRedirectUri();
return View("OAuthAsync");
}

var id = await this._githubOAuth.GetIdAsync(Request.Query["code"]);
var loginSuccess = this._githubOAuth.Login(id, Response.Cookies);

return Redirect(loginSuccess ? "/Master/" : "/Auth/OAuth");
}
}


 OAuth処理のベースをまとめた抽象クラス。ユーザにコードをもらうためにアクセスしてもらうURLや、サーバ側でコードを使ってトークン取得、ユーザ情報取得などがOAuth処理として普遍な機能なので実装されている。
using Microsoft.AspNetCore.Http;

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;


namespace OAuthProvider
{
abstract public class GithubOAuthBase
{
public GithubOAuthBase(string clientId, string clientSecret)
{
this._clientId = clientId;
this._clientSecret = clientSecret;
}

private string _clientId = "";

public string ClientId
{
get
{
if (String.IsNullOrEmpty(_clientId))
{
throw new ArgumentNullException(
"Github OAuth client ID isn't given."
);
}
return _clientId;
}
}

private string _clientSecret = "";

public string ClientSecret
{
set => _clientSecret = value;
get
{
if (String.IsNullOrEmpty(_clientSecret))
{
throw new ArgumentNullException(
"Github OAuth client secret isn't given."
);
}
return _clientSecret;
}
}

public string GetRedirectUri()
{
var uri = $"https://github.com/login/oauth/authorize?client_id={this.ClientId}";
return uri;

}


public async Task<string> GetIdAsync(string code)
{
// 取得したトークンを使ってGithubにユーザ情報を要求する
var token = await GetAccessTokenAsync(code);

var id = await GetUserId(token);

return id;
}

virtual public bool Login(string id, IResponseCookies cookies)
{
throw new NotImplementedException();
}

virtual public async Task<bool> LoginAsync(string id, IResponseCookies cookies)
{
throw new NotImplementedException();
}

private async Task<string> GetAccessTokenAsync(string code)
{
// 認証コードを使ってトークンを取得する
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "client_id", this.ClientId },
{ "client_secret", this.ClientSecret },
{ "code", code },
});
var response = await httpClient.PostAsync("https://github.com/login/oauth/access_token", content);
var responseBody = await response.Content.ReadAsStringAsync();
var jsonObj = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseBody);
var token = jsonObj["access_token"];

return token;
}

private async Task<string> GetUserId(string token)
{
// 取得したトークンを使ってGithubにユーザ情報を要求する
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

var uri = $"https://api.github.com/user?access_token={token}";
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Zenigata");
var response = await httpClient.GetAsync(uri);
var responseBody = await response.Content.ReadAsStringAsync();
var userInfo = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseBody);
var id = userInfo["id"];

return id;
}
}
}


 上記のクラスは抽象クラス。ユーザ情報をOAuthをやらせてくれるサイトから取ってきたら、その後の具体的な処理(認可を与えるべきか判断する、認可を与える)はサイトごとにことなる。それをLoginメソッドとして、↑の抽象クラスを継承した派生クラスで実装する。
using Microsoft.AspNetCore.Http;

using MongoDB.Driver;
using OAuthProvider;
using System;
using System.Security.Cryptography;
using tetsujin.Models;

namespace tetsujin
{
public class GithubOAuth : GithubOAuthBase
{
public GithubOAuth(string clientId, string clientSecret) : base(clientId, clientSecret) { }

/// <summary>
/// ログインを実行する
/// </summary>
/// <param name="id">ユーザID</param>
/// <param name="cookies">クッキー</param>
/// <returns>ログインの成否</returns>
override public bool Login(string id, IResponseCookies cookies)
{
var userCollection = DbConnection.Db.GetCollection<OAuthUser>(OAuthUser.CollectionName);
var filter = Builders<OAuthUser>.Filter.Eq("_id", id);
var master = userCollection.Find(filter).FirstOrDefault<OAuthUser>();
if (master == null) // ユーザが登録されていない場合
{
return false;
}
else // ユーザが登録されていた場合
{
// トークンを使ってセッションを開始
var token = GetToken();
var collection = DbConnection.Db.GetCollection<Session>(Session.CollectionName);
collection.InsertOne(new Session
{
Id = token,
CreatedAt = DateTime.Now
});

// cookieにsecure属性を付与
var cookieOption = new CookieOptions()
{
Secure = true
};
cookies.Append(OAuthUser.SessionCookie, token, cookieOption);

return true;
}
}

/// <summary>
/// ランダムトークンを取得
/// </summary>
/// <returns>ランダムトークン</returns>
private static string GetToken()
{
var rng = RandomNumberGenerator.Create();
var buff = new byte[25];
rng.GetBytes(buff);
return Convert.ToBase64String(buff);
}
}
}

 これでOAuth処理部分を、コントローラから任意のクラスに、うまく切り離すことができた。で、このOAuth処理を実装したクラスGithubOAuthのインスタンスをどうコントローラ内で参照できるようにするかという話である。クラスGithubOAuthはコンストラクタでClientIDとClientSecretを設定するので、静的クラスとしては使えず、インスタンス化する必要がある。
 インスタンスはいつ作る?リクエスト毎にコントローラのアクション内で…やるのは効率が悪い。インスタンスにはClientIDとClientSecretを保持してほしいが、これらはリクエスト毎に変えるものではない。アプリ立ち上げ時にあたえておくだけのもの。じゃあそれをやるために、ASP.NET Core MVCにあるDependency Injectionを使わせてもらう。
 ASP.NET Core MVCのDependency Injectionでコントローラにオブジェクトを渡すことができる。オブジェクトはクラスでもインスタンスでもいいし、インスタンスはその寿命を任意に与えられる。リクエスト毎か、アプリと同等の寿命を与えるかなど。今回はインスタンスにアプリと同等の寿命を与えるDependency Injectionにしている。
Startup.cs

public void ConfigureServices(IServiceCollection services)
{
services.Configure<GzipCompressionProviderOptions>
(options => options.Level = CompressionLevel.Fastest);
services.AddResponseCompression(options =>
{
options.Providers.Add<GzipCompressionProvider>();
});
services.AddMvc();

services.AddSingleton<HtmlEncoder>(
HtmlEncoder.Create(allowedRanges: new[] {
UnicodeRanges.BasicLatin,
UnicodeRanges.CjkSymbolsandPunctuation,
UnicodeRanges.Hiragana,
UnicodeRanges.Katakana,
UnicodeRanges.CjkUnifiedIdeographs
})
);

var githubClientId = Configuration.GetValue<string>("GITHUB_CLIENT_ID");
var githubClientSecret = Configuration.GetValue<string>("GITHUB_CLIENT_SECRET");
var githubOAuth = new GithubOAuth(githubClientId, githubClientSecret);
services.AddSingleton<GithubOAuth>(githubOAuth);
}

 これでコントローラにOAuth処理を提供してくれるインスタンスを渡すことができた。コントローラ作成時にしかインスタンス作成は行われないので不合理もない。Dependency Injectionで、コントローラにインスタンスを渡す満足な形にコードを持っていくことができた。

参考===============================
抽象クラス: http://ufcpp.net/study/csharp/oo_abstract.html
仮想メソッド: http://ufcpp.net/study/csharp/oo_polymorphism.html#virtual

Dependency Injection(オブジェクトの注入): https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/dependency-injection


comment: 0

ASP.NET Core MVCなアプリのコンテナで、ASP.NET Coreのバージョンを上げようとしたらビルドに失敗するようになったのと原因

ASP.NET Coreがどんどんバージョンを上げている。この度は2.0.0で運用していたものを2.0.6にしたところ、開発環境でのビルドで下記のエラーを吐くようになってしまった。

Error:

An assembly specified in the application dependencies manifest (MyApp.deps.json) was not found:
package: 'Microsoft.AspNetCore.Antiforgery', version: '2.0.2'
path: 'lib/netstandard2.0/Microsoft.AspNetCore.Antiforgery.dll'

This assembly was expected to be in the local runtime store as the application was published using the following target manifest files:
aspnetcore-store-2.0.6.xml

意訳:依存パッケージの○○がねーぞ

そういうわけで、ないと言われている依存パッケージをプロジェクトにインストールしたのだが、問題は解決しなかった。


 このエラーを吐き出しているプロセスはどこか。アプリの起動時である。ASP.NET Coreなアプリはビルド時に、依存パッケージを環境に任せてDLL化せずにおくか、それともDLLに入れてしまってスタンドアロンなDLLにするか選べる。デフォは環境任せでDLL化しない。たとえばコンテナでmicrosoft/aspnetcoreのイメージに、デフォでビルドしたDLLを配置すれば、コンテナ内に用意されているASP.NET CoreのDLLによって動くことになる。そんな依存パッケージのバージョン不一致にまつわるエラー。
 調べてみると、パッケージングのドキュメントにもこのことが書いてあった。
”マニフェストに示された依存関係のバージョンは、ランタイム パッケージ ストアの依存関係のバージョンに一致する必要があります。 ターゲット マニフェストの依存関係とランタイム パッケージ ストアに存在するバージョンの間に一致しないバージョンがあり、アプリにその展開に必要なバージョンのパッケージが含まれていない場合、アプリを開始することはできません。 この例外には、ランタイム パッケージ ストアのアセンブリを呼び出したターゲット マニフェストの名前があり、これは、この不一致のトラブルシューティングに役立ちます”
 エラーで指定されている依存パッケージをプロジェクトにインストールしてもエラーが解決したなった理由も書いてあった。
”展開に存在するマニフェストの依存関係を持つアプリケーションを展開する場合は (アセンブリは bin フォルダーに存在します)、ランタイム パッケージ ストアはそのアセンブリ用のホスト上では使用されません。 ホスト上のランタイム パッケージ ストアに存在するかに関係なく、bin フォルダーのアセンブリが使用されます”

 一つの、場合によってはdirtyな解決策は、スタンドアロンなDLL化である。.csprojで下記のように一行追加すれば、それでエラーは解消される。
<PropertyGroup>

<TargetFramework>netcoreapp2.0</TargetFramework>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>


これはASP.NET CoreのDLLを用意した環境にアプリのDLLを配置するならdirtyなやり方。

 今回こんなエラーが開発環境で出た原因は、Dockerイメージにあった。DockerイメージのASP.NET Coreのバージョンと、開発環境でアップグレードしたそれのバージョンが一致していなかったのが原因。Dockerイメージはずっと、以前にpullしたものを使っていたために、ASP.NET Coreのバージョンがそっちのほうで更新されていなかった。そういうわけで、解決法はDockerイメージを更新し、使用するDockerイメージとプロジェクト内で指定されているASP.NET Coreのバージョンを一致させることだった。
docker pull microsoft/aspnetcore:latest

 Dockerを使って開発している人は、一日の作業を開始するときにまず、Dockerイメージを更新したらいいかもしれない。
comment: 0