Top page  1/40
07
2019

ASP.NET CoreでAPIテスト (統合テスト) を行う

CATEGORY.NET
またASP.NET Coreネタ。今度はASP.NET Coreの自動テストで、APIレベルのテスト (統合テスト, Integration tests) を行う方法について。バージョンは引き続き2.1。

統合テストの方法については、公式ドキュメントにも以下のように説明がある。
が、いろいろ説明が冗長だったり、かと思えば足りなかったりと、最初にこれだけ見ても正直分かり辛い。
なので、実際に統合テストを行うのに必要な要点とかをまとめてみる。
(最終的なテストの実装例はこちら参照。)

テストプロジェクトの作成

まずテストプロジェクトの作成。Visual Studioで、公式のサンプルに合わせてxUnit.netのテストプロジェクトを作る。
(ただし、統合テストの仕組み自体はテストライブラリによらず共通と思われる。)

作成したプロジェクトでは、csprojファイルの冒頭を <Project Sdk="Microsoft.NET.Sdk.Web"> に差し替え、かつNuGetなりで以下のライブラリを参照している状態にする。
あと当然テスト対象のプロジェクトも参照する。
ここまでは普通のテストプロジェクトを作る場合とほぼ同じだと思う。

WebApplicationFactoryによるAPIテスト例

ASP.NET Coreの統合テストには、2.1から WebApplicationFactory というクラスが用意されている。
これにジェネリックでテスト対象プロジェクトの Startup クラスを指定すると、そのStartupを使ってテスト用のサーバーが立ち上がり、そこにリクエストを投げられるようになる。
単にAPIを呼ぶだけで良いのであれば、これを直接そのまま使うこともできる。

Tag: ASP.NET .NET

03
2019

Serilogでログを種類ごとに別のファイルに出力する

CATEGORY.NET
ASP.NET Coreネタ。ASP.NET Coreでは標準のロガーがあるが、そのままだとファイル出力ができないらしく、別途ライブラリを組み合わせるのが定番らしい(?)。
で、有名どころの「Serilog」を使ったのだが、ログを種類別に違うファイルに出力しようとしたら、ちょっと手間取ったので方法を書いておく。例によってStack Overflow等を参考にした。
最初に出来上がった設定を書いてしまうと、こんな感じ(使用例はこちら)。
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Logger",
"Args": {
"configureLogger": {
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "/var/log/local/aspnetcoreapi-example/app.log",
"rollingInterval": "Day"
}
}
],
"Filter": [
{
"Name": "ByExcluding",
"Args": {
"expression": "SourceContext = 'Honememo.AspNetCoreApiExample.Middlewares.AccessLogMiddleware' or StartsWith(SourceContext, 'Microsoft.EntityFrameworkCore.')"
}
}
]
}
}
},
{
"Name": "Logger",
"Args": {
"configureLogger": {
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "/var/log/local/aspnetcoreapi-example/access.log",
"rollingInterval": "Day"
}
}
],
"Filter": [
{
"Name": "ByIncludingOnly",
"Args": {
"expression": "SourceContext = 'Honememo.AspNetCoreApiExample.Middlewares.AccessLogMiddleware'"
}
}
]
}
}
},
{
"Name": "Logger",
"Args": {
"configureLogger": {
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "/var/log/local/aspnetcoreapi-example/sql.log",
"rollingInterval": "Day"
}
}
],
"Filter": [
{
"Name": "ByIncludingOnly",
"Args": {
"expression": "StartsWith(SourceContext, 'Microsoft.EntityFrameworkCore.')"
}
}
]
}
}
},
{
"Name": "Debug"
}
]
}
}
appsettings.json による設定。コードで設定する場合は同名のメソッドなどに読み替えてください。

ASP.NET Coreではロガー使用時に呼び出し元クラスを紐づけるものらしい(?)ので、そのクラス名でフィルタリングを行い、フィルタリングした結果をそれぞれのログファイルに出力している。
上の例では、AccessLogMiddleware のログを access.log ファイルに、EntityFrameworkCoreが出力するSQLログなどを sql.log ファイルに、それらを除外した残るログを app.log ファイルに出力している。
設定の構造的には、外側のロガーの設定の中に、Include/Excludeの設定をしたサブロガーがある形となっている。

クラスで分けられないケースは条件をもっと工夫する必要がありそうだけど、とりあえずこういう形で出力先を分けられますということで。

Tag: ASP.NET .NET

03
2019

ASP.NET CoreでDIを半自動化する「Scrutor」

CATEGORY.NET
引き続きASP.NET Coreネタ。ASP.NET Coreは標準でDIに対応しているのだが、標準のモノは何故か手動での依存関係登録が必須という面倒くさい仕組みになっている。
が、これを半分自動化して、クラス名やらのルールに基づいて一括制御できる「Scrutor」というライブラリがあったので、それを紹介する。
(実は公式ドキュメントの端っこでも少し紹介されてはいるのだが、自分は最初全然見つけられなかったので。)

まず、見つけた中で一番詳しい解説ページはこちら。以下の解説もベースはこちら。
ScrutorはDIライブラリではなく標準DIの依存関係登録に特化したライブラリとのこと。
なので、DIライブラリを丸々差し替えたりせず、あくまで標準ライブラリを使って開発が行えるのがメリット。

Scrutorを使ったDIの例はこんな感じ。Startup.cs の ConfigureServices() での依存関係登録がこういう風になる(使用例はこちら)。
services.Scan(scan => scan
.FromCallingAssembly()
.AddClasses(classes => classes.Where(type => type.Name.EndsWith("Repository") || type.Name.EndsWith("Service")))
.AsSelfWithInterfaces()
.WithScopedLifetime());
上記は大体メソッド名のまんまだが、解説すると実行元のアセンブリ(つまり自プロジェクト)にある XxxRepository または XxxService を、Scopedのライフサイクルで一括登録している。
AsSelfWithInterfaces() はクラスの実体とインタフェース双方をDIに登録する奴。

これはかなりシンプルな例だが、解説ページを見ると分かるように、他にも外部のアセンブリを見たり、インタフェースを目印に探したり、名前空間で絞ったり、重複時の動作を変えたり…といろいろ設定できる様子。

実際の現場では、この例のようにクラス名などで絞り込めることが多いので、この仕組みで十分実用的だと思う。
(一部例外的な奴だけ手動で登録すればいいのだし。)
クラスが増えるたびに手で全部登録するのは流石に非効率的すぎるので、こういうのを使って効率化していきたい。

Tag: ASP.NET .NET

01
2019

ASP.NET Core IdentityテーブルのIndex column size too largeの対処

CATEGORY.NET
ASP.NET Coreネタ二個目。ASP.NET Core標準のユーザー認証(?)のASP.NET Core Identityは、勝手にユーザーテーブルとか作ってくれるわけだが、ちょっと古いMySQLのutf8mb4データベースだと、自動生成されるテーブル定義が有名な「Index column size too large. The maximum column size is 767 bytes.」に引っかかってしまってマイグレーションエラーになる。

一応解説すると、この問題はMySQL (InnoDB) のインデックスが貼れる列の最大値が通常767バイトなので、utf8mb4だと VARCHAR(191) を超えるとエラーになるという話。自動生成されるテーブルは、列が VARCHAR(255) なのでアウトである。

最新のMySQLなら最大値が上がってて大丈夫とかそういう話もあるけど、今回はアプリ側で列サイズを191文字まで削って対応しようとした。
が、自動生成のテーブルはEntityクラスが無いので、いつものアノテーションでは列サイズが設定できない。
どうしようかと思ったら、Fluent APIなら普通に自動生成のテーブルもカスタマイズできたのでそれで対応した。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<IdentityUser<int>>(entity =>
{
entity.Property(m => m.Email).HasMaxLength(191);
entity.Property(m => m.NormalizedEmail).HasMaxLength(191);
entity.Property(m => m.NormalizedUserName).HasMaxLength(191);
entity.Property(m => m.UserName).HasMaxLength(191);
});
modelBuilder.Entity<IdentityRole<int>>(entity =>
{
entity.Property(m => m.Name).HasMaxLength(191);
entity.Property(m => m.NormalizedName).HasMaxLength(191);
});
modelBuilder.Entity<IdentityUserLogin<int>>(entity =>
{
entity.Property(m => m.LoginProvider).HasMaxLength(191);
entity.Property(m => m.ProviderKey).HasMaxLength(191);
});
modelBuilder.Entity<IdentityUserToken<int>>(entity =>
{
entity.Property(m => m.LoginProvider).HasMaxLength(191);
entity.Property(m => m.Name).HasMaxLength(191);
});
}
こんな感じ。Entityを継承して使っている場合はそちらを指定する。

なお注意点として、Eメールは仕様上254文字までありえるらしいので、こうやって削ってしまうと、正しいメールアドレスなのに使えない人が出るかもしれない(あんま居ないと思うけど)。
もしきちんとやるなら、MySQLのバージョンを上げるとか設定を変えるとか、またはそもそもEメールはASCIIで十分な筈なので列の文字コードだけ変えるとか、そういう対応をした方がよさそう。

Tag: ASP.NET .NET MySQL

01
2019

ASP.NET CoreでHttpRequest/HttpResponseのBodyを参照する

CATEGORY.NET
ここ最近、他所のプロジェクトのヘルプでASP.NET Coreを触ってたのだけど、何点かハマったところがあって、かつ日本語の情報が無い(?)のもあったのでそういうのメモっとく。なおバージョンは2.1。初めはコンテンツボディの取得法について。

例えば詳細なアクセスログでリクエスト/レスポンスのボディまで出力したい、ってことはちょくちょくあると思う。
で、今回もそれやろうとしたのだけれど、デフォルトでは(たぶん性能対策のため)参照できないようだったので、Stack Overflowの丸写しだけどやり方解説。
まず HttpRequest については EnableBuffering() というメソッドを先に呼んでおけば、リクエストボディのバッファリングが有効になって、以後普通に再読み込み可能なStreamとして使用可能になる模様。簡単である。

一方面倒なのが HttpResponse の方。こちらは同じようなメソッドが用意されていないので(何故!?)、こんな感じに無理やりBodyのStreamを MemoryStream に差し替えてやる必要がある(以下ミドルウェアでの例)。
public async Task Invoke(HttpContext context)
{
var responseStream = context.Response.Body;
try
{
using (var memoryStream = new MemoryStream())
{
context.Response.Body = memoryStream;
await this.next(context);

memoryStream.Position = 0;
Console.WriteLine(new StreamReader(memoryStream).ReadToEnd());

memoryStream.Position = 0;
await memoryStream.CopyToAsync(responseStream);
}
}
finally
{
context.Response.Body = responseStream;
}
}
個人的には、いろんなとこで必要になりそうなので、これだけ専用のミドルウェアに抜き出してしまった方がよいと思う。そうすれば以後使う側は何も意識する必要がなくなるので(ミドルウェア実装例ボディ参照例)。

ただ、MemoryStream に一度保存するという事は、巨大なレスポンスボディが有ったりするとメモリ使用量が危なそう。
本番環境で使用する場合はご注意くださいm(__)m

Tag: ASP.NET .NET