Entity Framework Core で色々な SQL を投げてみる 2
- はじめに
- SELECT 2
- [LIKE]前方一致で検索する
- [LIKE]中間一致で検索する
- [LIKE]後方一致で検索する
- 正規表現で検索する
- [BETWEEN] 価格帯で検索する
- [WHERE IN] 価格リストに合致するレコードを検索する
- [EXISTS] 価格が 5000 円以上のレコードが存在するか
- [CASE] CASE 式を使う
- [UNION ALL] 2 つのテーブルの検索結果を足し合わせる(重複あり)
- [UNION] 2 つのテーブルの検索結果を足し合わせる(重複なし)
- [INTERSECT] 2 つのテーブルの共通部分を取り出す
- [EXCEPT] 2 つのテーブルで共通しないレコードのみを取り出す
- [INNER JOIN] 複数のテーブルを参照する(内部結合)
- [LEFT OUTER JOIN] 複数のテーブルを参照する(外部結合)
はじめに
SELECT 文の続きです。
今回も SQL ゼロからはじめるデータベース操作 を参考に、色々試してみますよ。
SELECT 2
[LIKE]前方一致で検索する
コード
await _context.Book.Where(b => b.Name.StartsWith("Entity")).ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b WHERE b."Name" LIKE 'Entity%'
[LIKE]中間一致で検索する
コード
await _context.Book.Where(b => b.Name.Contains("Framework")).ToListAsync();
発行される SQL
SELECT b."BookId", b."Name", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b WHERE STRPOS(b."Name", 'Framework') > 0
むむむ。発行したかった SQL は「 WHERE b."Genre" LIKE '%Entity%' 」だったのですが。。。
あくまで %QUERY% にこだわるなら、もう少し別の方法もあるかもしれませんね。
[参照]
[LIKE]後方一致で検索する
コード
await _context.Book.Where(b => b.Name.EndsWith("Action")).ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b WHERE RIGHT(b."Name", LENGTH('Action')) = 'Action'
むぅ。。。これも違う。
結果が正しいので良い。。。のかな?
正規表現で検索する
コード
System.Text.RegularExpressions.Regex regex1 = new System.Text.RegularExpressions.Regex("Action$"); await _context.Book.Where(b => regex1.IsMatch(b.Name)).ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b
Where ごと消えた/(^o^)\
この辺は発行された SQL の内容で確認するのが難しそうですねぇ。。。
warn: Microsoft.EntityFrameworkCore.Query[20500] The LINQ expression 'where __regex1_0.IsMatch([b].Name)' could not be translated and will be evaluated locally.
[BETWEEN] 価格帯で検索する
コード
await _context.Book.Where(b => b.Price >= 500m && b.Price <= 5000m).ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b WHERE (b."Price" >= 500.0) AND (b."Price" <= 5000.0)
ですよね~、という結果ですが、どうやら Linq に BETWEEN 的なものはなさそうです。
[WHERE IN] 価格リストに合致するレコードを検索する
コード
await _context.Book.Where(b => new []{10m, 500m, 7000m}.Contains(b.Price)).ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b WHERE b."Price" IN (10.0, 500.0, 7000.0)
なお、下記のように配列部分を変更しても同じ結果となりました。
var prices = new[] {10m, 500m, 7000m}; await _context.Book.Where(b => prices.Contains(b.Price)).ToListAsync();
[EXISTS] 価格が 5000 円以上のレコードが存在するか
コード
_context.Book.Any(b => b.Price >= 5000m);
発行される SQL
SELECT CASE WHEN EXISTS ( SELECT 1 FROM "Book" AS b WHERE b."Price" >= 5000.0) THEN TRUE::bool ELSE FALSE::bool END
おっと。ここで CASE が登場しましたね。
THEN 以降は、 C# と SQL で型を合わせるためなのでしょうか。
[CASE] CASE 式を使う
コード
_context.Book.Select(b => (b.Price >= 5000m)? 1:0 ).ToList();
発行される SQL
SELECT CASE WHEN b."Price" >= 5000.0 THEN 1 ELSE 0 END FROM "Book" AS b
Linq には Case が無いようです。
そのため Entity Framework Core でも使えない。。。と思いきや、参考演算子にすると CASE 式に変換されれるようです。
なお上記サンプルの雑さは見ないことにしてくださいorz
[UNION ALL] 2 つのテーブルの検索結果を足し合わせる(重複あり)
準備
Book テーブルと同じレイアウトで Book2 というテーブルを作ります。
Book2
CREATE TABLE "Book2" ( "BookId" integer PRIMARY KEY, "Genre" text, "Name" text, "Price" numeric, "ReleaseDate" timestamp without time zone, "Thumbnail" bytea)
ここにいくつかのレコードは Book と重複する内容で、雑にレコードを追加します。
C# 側でも、 Model クラスと Context クラスへの追加を行います。
Book2.cs
using System; namespace EfCoreNpgsqlSample.Models { public class Book2 { [Key] public int BookId { get; set; } public string Name { get; set; } public DateTime ReleaseDate { get; set; } public string Genre { get; set; } public decimal Price { get; set; } public byte[] Thumbnail { get; set; } } }
- BookId に [Key] がついているのは、 CREATE TABLE 時に BookId を PRIMARY KEY として登録したためです(外すとエラーになります)。
EfCoreNpgsqlSampleContext.cs
using Microsoft.EntityFrameworkCore; namespace EfCoreNpgsqlSample.Models { public class EfCoreNpgsqlSampleContext: DbContext { public EfCoreNpgsqlSampleContext(DbContextOptions< EfCoreNpgsqlSampleContext> options) : base(options) { } public DbSet< Book> Book { get; set; } public DbSet< Book2> Book2 { get; set; } } }
で、 Book 、 Book2 への検索結果を足し合わせたい、というのが目標です。
SQL はこちら。
SELECT "BookId", "Genre", "Name", "Price", "ReleaseDate" FROM "Book" UNION ALL SELECT "BookId", "Genre", "Name", "Price", "ReleaseDate" FROM "Book2"
SQL はこれで良いのですが、 C# では Book と Book2 が別のクラスとして扱われるので、これをそのまま実行することができません。
Book2 の結果を受け取った後、 Book に変換したあと Concat で結合します。
コード
IQueryable< Book> books1 = _context.Book .Where(b => b.Name != null); IQueryable< Book> books2 = _context.Book2 .Where(b => b.Name != null) .Select(b => new Book { BookId = b.BookId, Name = b.Name, Genre = b.Genre, Price = b.Price, ReleaseDate = b.ReleaseDate }); await books1.Concat(books2).ToListAsync();
Book テーブルと Book2 テーブルに対してそれぞれ SELECT を実行しているため、発行される SQL も 2 つに分かれています。
発行される SQL
info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (7ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b WHERE b."Name" IS NOT NULL info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT b0."BookId", b0."Name", b0."Genre", b0."Price", b0."ReleaseDate" FROM "Book2" AS b0 WHERE b0."Name" IS NOT NULL
[UNION] 2 つのテーブルの検索結果を足し合わせる(重複なし)
Concat で結合するだけだと、重複するデータがある場合もそのまま出力されます。
重複するデータを取り除く方法として Distinct があります。
が、そのままだと Book テーブルと Book2 テーブルに重複があっても別物として扱われます。
ということで、 Book クラスの Equals をオーバーライドします。
Book.cs
using System; using System.ComponentModel.DataAnnotations; namespace EfCoreNpgsqlSample.Models { public class Book { [Key] public int BookId { get; set; } public string Name { get; set; } [DataType(DataType.Date)] public DateTime ReleaseDate { get; set; } public string Genre { get; set; } public decimal Price { get; set; } public byte[] Thumbnail { get; set; } public override bool Equals(object obj) { var compareBook = obj as Book; if (compareBook == null) { return false; } return compareBook.BookId == BookId; } public override int GetHashCode() { return BookId; } } }
GetHashCode で、戻り値が static じゃないよ!と警告が出たりしていますが、今回は見なかったことに。。。(良い子はマネしない)
また、今回は BookId のみで比較していますが、実際の用途的には Name や Genre で比較したほうが良さそうです。
なお SQL の UNION では「 SELECT "BookId", "Genre", "Name", "Price", "ReleaseDate" ~」なら BookId 、 Genre 、 Name 、 Price 、 ReleaseDate の全部が一致しているかどうかを見ているようです。
あとは Distinct で重複データが取り除けます。
await books1.Concat(books2).Distinct().ToListAsync();
[INTERSECT] 2 つのテーブルの共通部分を取り出す
Linq にもそのままズバリ Intersect が存在するため、これを使えば実現できます。
コード
await books1.Intersect(books2).ToListAsync();
先ほどと同じく SQL には反映されないため、発行された SQL は省略します。
[EXCEPT] 2 つのテーブルで共通しないレコードのみを取り出す
こちらも Except が存在するため楽に実現できますね。
コード
await books1.Except(books2).ToListAsync();
注意点?としては、上記の例では Book テーブルの持つレコードがベースとなるため、 Book1 にのみ共通しないレコードが存在する場合、 0 件で返ってきます。
[INNER JOIN] 複数のテーブルを参照する(内部結合)
準備
Author テーブルを作り、 Book テーブルに AuthorId 列を追加します。
Author
CREATE TABLE "Author" ( "AuthorId" integer PRIMARY KEY, "Name" text)
Book
ALTER TABLE "Book" ADD COLUMN "AuthorId" integer
C# 側でも、 Model クラスと Context クラスへの追加を行います。
Author.cs
using System.ComponentModel.DataAnnotations; namespace EfCoreNpgsqlSample.Models { public class Author { [Key] public int AuthorId { get; set; } public string Name { get; set; } } }
Book.cs
using System; using System.ComponentModel.DataAnnotations; namespace EfCoreNpgsqlSample.Models { public class Book { [Key] public int BookId { get; set; } public int AuthorId { get; set; } ~省略~ } }
EfCoreNpgsqlSampleContext.cs
using Microsoft.EntityFrameworkCore; namespace EfCoreNpgsqlSample.Models { public class EfCoreNpgsqlSampleContext: DbContext { public EfCoreNpgsqlSampleContext(DbContextOptionsoptions) : base(options) { } public DbSet< Book> Book { get; set; } public DbSet< Book2> Book2 { get; set; } public DbSet< Author> Author { get; set; } } }
Linq には Join が用意されているため、これを使います。
コード
// 戻り値の型は IQueryable< anonymous type> from book in _context.Book join author in _context.Author on book.AuthorId equals author.AuthorId select new { BookId = book.BookId, AuthorId = author.AuthorId, BookName = book.Name, AuthorName = author.Name, };
発行される SQL
SELECT book."BookId", author."AuthorId", book."Name" AS "BookName", author."Name" AS "AuthorName" FROM "Book" AS book INNER JOIN "Author" AS author ON book."AuthorId" = author."AuthorId"
無名クラスとして結合した値を受け取ることができるため、一時的に使うだけならこの形が便利ですね。
なお唐突にクエリ式になったのは、 Join についてはこっちの方が理解しやすそうだったためです。
[LEFT OUTER JOIN] 複数のテーブルを参照する(外部結合)
※ 2019/02/26 23:30 更新
Book テーブルに Author テーブルに存在しない AuthorId を設定した場合もエラーが起きないよう修正しました。
コード
var results = from book in _context.Book join author in _context.Author on book.AuthorId equals author.AuthorId into gj from ba in gj.DefaultIfEmpty() select new { BookId = book.BookId, AuthorId = book.AuthorId, BookName = book.Name, AuthorName = (ba == null)? "": ba.Name, };
発行される SQL
SELECT book."BookId", book."AuthorId", book."Name" AS "BookName", CASE WHEN author."AuthorId" IS NULL THEN '' ELSE author."Name" END AS "AuthorName" FROM "Book" AS book LEFT JOIN "Author" AS author ON book."AuthorId" = author."AuthorId"
SQL に反映されていない。。。
が、これで Book テーブルにあって、 Author テーブルに登録されていない AuthorId を持つレコードが結果に出力されるようになりました。
またまた長くなってきたので切ります。
Entity Framework Core で色々な SQL を投げてみる 1
- はじめに
- メモ
- SELECT
- 全件取得
- カラムを指定して検索
- [LIMIT] 条件に合致するレコードを一件だけ取得する
- [LIMIT] 条件に合致するレコードを三件(二件以上)取得する
- [DISTINCT] ジャンルが重複するデータを除く
- [演算子] SELECT で演算子を使う
- [演算子] WHERE で演算子を使う
- [COUNT] レコード数をカウントする
- [COUNT] ジャンルが NULL でないレコード数をカウントする
- [AVG] 価格の平均を求める
- [MAX] 価格の最大値を求める
- [MIN] 価格の最小値を求める
- [GROUP BY???] ジャンルでグループ分けする
- [HAVING] グループ化したレコードをフィルタリングする
- [ORDER BY] レコードをリリース日でソートする(昇順)
- [ORDER BY] レコードをリリース日でソートする(降順)
はじめに
- ASP.NET Core のプロジェクトに EntityFrameworkCore で Model を追加してみた話
- Entity Framework Core のスキャフォールド・マイグレーションで生成されたものを見たい
- 【ASP.NET Core】【Entity Framework Core】PostgreSQL に接続してみる
さて、 PostgreSQL に接続できたことだし、 DbContext を追うぞ~と思ったのですが、あまりに知識が雑すぎたためどこから手を付けていいかもわからない状態に。。。orz
引き続き追いかけてはいくのですが、まずは下記の書籍を参考に、 Entity Framework Core で色々な SQL 文を発行してみたいと思います。
まずは SELECT から。
メモ
とその前に小ネタをメモっておきます。
Model クラスとテーブルに差異がある場合
- Model クラスに DB のテーブルに無い値(プロパティ)がある場合 → そのプロパティを使っている、使っていないにかかわらず Exception が発生します。
- DB のテーブルには存在するが、 Model クラスにプロパティが無い値(カラム)がある場合 → 問題なし。ただ値が取れないだけ。
また、 テーブルのカラム名と Model クラスのプロパティの名前が異なる場合、 [ Column("")] で指定することができます。
SELECT
全件取得
Book テーブルのデータを全件取得します。
コード
await _context.Book.ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b
コンソールに出力されるのが便利ですね。
カラムを指定して検索
カラム BookId のみを指定して取得してみます。
コード
await _context.Book.Select(b => b.BookId).ToListAsync();
発行される SQL
SELECT b."BookId" FROM "Book" AS b
[WHERE] 価格が 3000 円以上のレコードを取得する
コード
await _context.Book.Where(b => b.Price >= 3000).ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b WHERE b."Price" >= 3000.0
[LIMIT] 条件に合致するレコードを一件だけ取得する
コード
_context.Book.FirstOrDefault(b => b.Price >= 3000);
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b WHERE b."Price" >= 3000.0 LIMIT 1
[LIMIT] 条件に合致するレコードを三件(二件以上)取得する
コード
await _context.Book.Where(b => b.Price >= 3000).Take(3).ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b WHERE b."Price" >= 3000.0 LIMIT @__p_0
結果としては FirstOrDefault と同じように LIMIT が使われるのですが、パラメーターが不思議な値になっています。
(なお Take の引数を変えてもコンソール出力の内容は変わりませんでした)
コンソール出力後に置き換えられるのでしょうか。
※なお TakeLast はサポート外らしく、使用すると Exception が発生します。
[DISTINCT] ジャンルが重複するデータを除く
コード
await _context.Book.Select(b => b.Genre).Distinct().ToListAsync();
発行される SQL
SELECT DISTINCT b."Genre" FROM "Book" AS b
[演算子] SELECT で演算子を使う
コード
await _context.Book.Select(b => b.Price * 1.08m).ToListAsync();
発行される SQL
SELECT b."Price" * 1.08 FROM "Book" AS b
[演算子] WHERE で演算子を使う
コード
await _context.Book.Where(b => (b.Price * 1.08m) >= 3000.0m).ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b WHERE (b."Price" * 1.08) >= 3000.0
[COUNT] レコード数をカウントする
コード
await _context.Book.CountAsync();
発行される SQL
SELECT COUNT(*)::INT FROM "Book" AS b
[COUNT] ジャンルが NULL でないレコード数をカウントする
コード
await _context.Book.CountAsync(b => b.Genre != null);
発行される SQL
SELECT COUNT(*)::INT FROM "Book" AS b WHERE b."Genre" IS NOT NULL
「 SELECT COUNT("Genre") FROM "Book" 」とできると、同様のことが WHERE 無しでできるのですが、少なくとも CountAsync() を使う場合、「 COUNT(*)::INT 」となってしまうようです。
[SUM] 価格の合計を求める
コード
await _context.Book.SumAsync(b => b.Price);
発行される SQL
SELECT SUM(b."Price") FROM "Book" AS b
[AVG] 価格の平均を求める
コード
await _context.Book.AverageAsync(b => b.Price);
発行される SQL
SELECT AVG(b."Price") FROM "Book" AS b
[MAX] 価格の最大値を求める
コード
await _context.Book.MaxAsync(b => b.Price);
発行される SQL
SELECT MAX(b."Price") FROM "Book" AS b
[MIN] 価格の最小値を求める
コード
await _context.Book.MinAsync(b => b.Price);
発行される SQL
SELECT MIN(b."Price") FROM "Book" AS b
[GROUP BY???] ジャンルでグループ分けする
コード
await _context.Book.GroupBy(b => b.Genre).ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b ORDER BY b."Genre"
ORDER BY ???
発行したかったのは
SELECT "Genre" FROM "Book" GROUP BY "Genre"
です。
結果としてはどちらもジャンルでグループ分けできているようなのですが。。。
なお、当然ながら発行された SQL の内容を PgAdmin で実行すると、ジャンルでソートされた結果が表示されるだけです。
と思ったら、コンソールに下記の警告が出力されていました。
warn: Microsoft.EntityFrameworkCore.Query[20500] The LINQ expression 'GroupBy([b].Genre, [b])' could not be translated and will be evaluated locally.
どうやら、上記の SQL が発行される前に C# 側で何か処理を行っているようです。
同じようなことを Issue で挙げている方もいたようですが。。。
https://github.com/aspnet/EntityFrameworkCore/issues/9964
これについては、次回以降でもう少し見てみたいと思います(挫折しなければ)。
[HAVING] グループ化したレコードをフィルタリングする
コード
await _context.Book.GroupBy(b => b.Genre) .Where(b => string.IsNullOrEmpty(b.Key) == false) .ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b ORDER BY b."Genre"
あれ?発行される SQL がさっきと変わってない(´・ω・`)
結局 GroupBy がそのまま変換されないため、そこに対する Where も変換はされないようです(結果には反映されますが)。
ちなみに Where についても警告が出力されています。
warn: Microsoft.EntityFrameworkCore.Query[20500] The LINQ expression 'GroupBy([b].Genre, [b])' could not be translated and will be evaluated locally. warn: Microsoft.EntityFrameworkCore.Query[20500] The LINQ expression 'where (IsNullOrEmpty([b].Key) == False)' could not be translated and will be evaluated locally.
[参照]
[ORDER BY] レコードをリリース日でソートする(昇順)
コード
await _context.Book.OrderBy(b => b.ReleaseDate).ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b ORDER BY b."ReleaseDate"
GroupBy でも(なぜか) ORDER BY が発行されていたので違いがよくわからない感じになっていますが(苦笑)、割とそのまま書けますね。
[ORDER BY] レコードをリリース日でソートする(降順)
コード
await _context.Book.OrderByDescending(b => b.ReleaseDate).ToListAsync();
発行される SQL
SELECT b."BookId", b."Genre", b."Name", b."Price", b."ReleaseDate", b."Thumbnail" FROM "Book" AS b ORDER BY b."ReleaseDate" DESC
長くなってきたのでいったん切ります。
次回は SQL ゼロからはじめるデータベース操作 の六章ぐらいから。
【ASP.NET Core】【Entity Framework Core】PostgreSQL に接続してみる
はじめに
Entity Framework Core の謎を追うシリーズの途中ですが、ここまで使ってきた SQLite ではなく、 PostgreSQL を使うことにしました。
PostgreSQL に興味があるとか、 PgAdmin4 を使ってみたいとかはもちろんあるのですが、唐突に話が変わる場合は。。。お察しくださいw
準備
今回 PostgreSQL は Docker のものを使用しました。
https://hub.docker.com/_/postgres
Description の内容を元に設定したのですが、この辺りはもう少しちゃんと内容を理解してから取り上げたいと思います。
(今はとりあえず動いてる状態なので)
PgAdmin はローカルにインストールしたものを使用しています。
接続する
基本的な内容は SQLite を使った時と同じです。
NuGet パッケージとして、 SQLite の代わりに Npgsql.EntityFrameworkCore.PostgreSQL をインストールしました。
https://github.com/npgsql/Npgsql.EntityFrameworkCore.PostgreSQL/blob/dev/doc/index.md
Connection string などの設定は、 SQLite と同じく Startup でできる他、上記を見ると DbContext クラスに書いているようです。
Startup.cs に書く場合
EfCoreNpgsqlSampleContext.cs
using Microsoft.EntityFrameworkCore; namespace EfCoreNpgsqlSample.Models { public class EfCoreNpgsqlSampleContext: DbContext { public EfCoreNpgsqlSampleContext(DbContextOptions< EfCoreNpgsqlSampleContext> options) : base(options) { } public DbSet< Book> Book { get; set; } } }
Startup.cs
~省略~ public void ConfigureServices(IServiceCollection services) { services.Configure< CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddDbContext< EfCoreNpgsqlSampleContext>(options => options.UseNpgsql("Host=localhost;Database=DbName;Username=postgres;Password=XXXX")); services.AddMvc(); } ~省略~
DbContext に書く場合
EfCoreNpgsqlSampleContext.cs
using Microsoft.EntityFrameworkCore; namespace EfCoreNpgsqlSample.Models { public class EfCoreNpgsqlSampleContext: DbContext { public EfCoreNpgsqlSampleContext(DbContextOptions< EfCoreNpgsqlSampleContext> options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql("Host=localhost;Database=DbName;Username=postgres;Password=XXXX"); public DbSet< Book> Book { get; set; } } }
Startup.cs
~省略~ public void ConfigureServices(IServiceCollection services) { services.Configure< CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddEntityFrameworkNpgsql() .AddDbContext< EfCoreNpgsqlSampleContext>() .BuildServiceProvider(); services.AddMvc(); } ~省略~
後者は Connection string が実際にそれを必要とする DbContext に書ける、という利点はあるものの、二か所に分割して書くことになるため面倒にも感じましたが、他にも利点はあるのでしょうか。
ともあれ、これで PostgreSQL に接続し、データの入出力ができるようになりました。
NuGet パッケージの差し替えと数か所のコードの変更だけで DB が切り替えられる、というのはやはり便利ですね。
Entity Framework Core のスキャフォールド・マイグレーションで生成されたものを見たい
はじめに
前回、半年間溜め込んでいた内容をようやくブログとして書くことができて、スッキリしている今日この頃ですw
今回は生成されたファイルを覗いてみることにします。
マイグレーションはスキャフォールドを必要とするか
前回はスキャフォールドで View 、 Controller を生成したあとマイグレーションを実行していました。
ただ、 Controller はともかく View は不要( Web API とか)な場合、わざわざスキャフォールドしなくても Controller だけ作れば良いような気もします。
ということで確認したところ、特にスキャフォールドを実行していなくてもマイグレーションできました。
ということで、状況に合わせて使い分けるのが良さそうですね。
生成した・されたファイルを見てみる
さて、前回 Empty テンプレートの ASP.NET Core プロジェクトに追加した、またはスキャフォールド・マイグレーションで生成されたファイルを見てみることにします。
Controllers L MoviesController.cs Migrations L 20190214222750_AddMovie.cs L 20190214222750_AddMovie.Designer.cs L EfCoreSampleContextModelSnapshot.cs Models L EfCoreSampleContext.cs L Movie.cs Views L Movies L Create.cshtml L Delete.cshtml L Details.cshtml L Edit.cshtml L Index.cshtml L Shared L _ValidationScriptsPartial.cshtml
View の部分は基本的に Razor で Form を出しているだけなのでスキップすることにして、それ以外の部分を追いかけてみたいと思います。
Movie.cs を見る
C# 側でデータを保持し、マイグレーション時に DB のテーブル生成するのにも使われるクラスです。
Movie.cs
using System; using System.ComponentModel.DataAnnotations; namespace EfCoreSample.Models { public class Movie { public int MovieId { get; set; } public string Title { get; set; } [DataType(DataType.Date)] public DateTime ReleaseDate { get; set; } public string Genre { get; set; } public decimal Price { get; set; } } }
DataType
ただデータを保持するクラス、ということで、そんなに変わったところも見られません。
唯一気になるのが「[DataType(DataType.Date)]」の属性。
↑などを見ると、どうやら Razor で下記のようなフォームを表示するのに使われているようです。
試しにコメントアウトしてみると、他と同じようにただ入力エリアが表示されていました。
DB 側に関係があるかはよく分かっていませんが。
EfCoreSampleContext.cs を見る
次は EfCoreSampleContext.cs です。
Entity Framework Core in Action によると DB へのアクセスや設定を担う、ということで、今回の中で一番重要なクラスと言えそうです。
EfCoreSampleContext.cs
using Microsoft.EntityFrameworkCore; namespace EfCoreSample2.Models { public class MovieContext : DbContext { public MovieContext (DbContextOptions< MovieContext> options) : base(options) { } public DbSet< Movie> Movie { get; set; } } }
この中に含まれるキーワードとしては下記の 3 つがあります。
- DbContext
- DbContextOptions
- DbSet
DbContext Class (Microsoft.EntityFrameworkCore) などを見ると、 DB との直接的なやり取りをしているのは DbContext クラスとのこと。
2.の DbContextOptions は、接続する DB の情報などを渡すのに使われるようです。
https://github.com/JonPSmith/EfCoreInAction/blob/Chapter01/MyFirstEfCoreApp/AppDbContext.cs
(関係ないですが、ブランチを本の章を切り替えるのに使うのが新鮮というか、ちょっとびっくりしました)
これをどこから渡しているかというと、 Startup > ConfigureServices() です。
Startup.cs
~省略~ public void ConfigureServices(IServiceCollection services) { services.Configure< CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddDbContext< EfCoreSampleContext>(options => options.UseSqlite(Configuration.GetConnectionString("MovieContext"))); services.AddMvc(); } ~省略~
SQLite の使用と Data Source として MvcMovie.db が指定されています。
appsettings.json
~省略~ "ConnectionStrings": { "MovieContext": "Data Source=MvcMovie.db" } }
3.の DbSet は、 DbContext の Set() で Entity クラスとして登録するのに使われるようです。
また、スキャフォールドで生成された MoviesController.cs を見てみると、 DB アクセスの部分は MovieContext(DbContext) 、または DbSet を検索したり、値を取得しているだけのようです。
MoviesController.cs
~省略~ [Route("/Movies/Details/{*id}")] public async Task< IActionResult> Details(int? id) { if (id == null) { return NotFound(); } var movie = await _context.Movie .FirstOrDefaultAsync(m => m.MovieId == id); if (movie == null) { return NotFound(); } return View(movie); } ~省略~ [HttpPost] [Route("/Movies/Create")] [ValidateAntiForgeryToken] public async Task< IActionResult> Create([Bind("MovieId,Title,ReleaseDate,Genre,Price")] Movie movie) { if (ModelState.IsValid) { _context.Add(movie); await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } return View(movie); } ~省略~
ということを考えると、 Entity Framework Core がどんなことをしてくれるのかを知るためには DbContext を中心に調べていくのが良さそうです。
処理を調べるにはドキュメントとコードを見るのが一番、だと思いますが、 Entity Framework Core のコードは下記にあります。
その中の DbContext はこちら。
https://github.com/aspnet/EntityFrameworkCore/blob/master/src/EFCore/DbContext.cs
次回はこれをたどってみることにします。
参照
ASP.NET Core のプロジェクトに EntityFrameworkCore で Model を追加してみた話
はじめに
ようやく ASP.NET Core MVC の Model の話です。
Empty で ASP.NET Core のプロジェクトを作り、 Controller クラスだけを作った状態で、下記チュートリアルを参考に Model を追加してみることにします。
スキャフォールド
今回は ASP.NET Core のプロジェクトを Empty のテンプレートで生成し、そこに追加することにしたいと思います。
チュートリアルにのっとり、 Models > Movie というクラスを作ってみます。
Movies.cs
using System; namespace WebApplication1.Models { public class Movie { public int ID { get; set; } public string Title { get; set; } public DateTime ReleaseDate { get; set; } public string Genre { get; set; } public decimal Price { get; set; } } }
DbContext クラスを追加します。
DB の設定などを行うためのクラスですが、ここではほぼ空の状態で作成しておきます。
using Microsoft.EntityFrameworkCore; namespace EfCoreSample.Models { public class EfCoreSampleContext : DbContext { public EfCoreSampleContext (DbContextOptionsoptions) : base(options) { } public DbSet Movie { get; set; } } }
このチュートリアルでは、既存の DB を使うのではなくコードから生成します。
その名前は appsettings.json で指定しています。
appsettings.json
{ ~省略~ "ConnectionStrings": { "MovieContext": "Data Source=MvcMovie.db", } }
必要なパッケージを NuGet でインストールします(どれもバージョンは 2.2.2 にしました)。
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SQLite
- Microsoft.EntityFrameworkCore.Design
- Microsoft.VisualStudio.Web.CodeGeneration.Design
Startup クラスで先ほど作成した DBContext を追加します。
Startup.cs
using EfCoreSample.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace EfCoreSample { public class Startup { public IConfiguration Configuration { get; } public Startup(IConfiguration configuration) { Configuration = configuration; } public void ConfigureServices(IServiceCollection services) { services.Configure< CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddDbContext< EfCoreSampleContext>(options => options.UseSqlite(Configuration.GetConnectionString("MovieContext"))); services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc(); } } }
そしてスキャフォールド、…といきたいところですが、 Rider では GUI ではスキャフォールドできず、 EntityFramework のコマンドを実行する必要があるようです。
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc EfCoreSampleContext --relativeFo lderPath Controllers --useDefaultLayout --referenceScriptLibraries
エラー
スキャフォールドに成功すると、 View、Controllerにファイルが追加されます。
が、 Views/Movies/Create.cshtml と Views/Movies/Edit.cshtml でエラーが。。。
Create.cshtml
@model EfCoreSample.Models.Movie @{ ViewData["Title"] = "Create"; } < h1>Create< /h1> < h4>Movie< /h4> < hr /> < div class="row"> < div class="col-md-4"> < form asp-action="Create"> < div asp-validation-summary="ModelOnly" class="text-danger">< /div> < div class="form-group"> < label asp-for="Title" class="control-label">< /label> < input asp-for="Title" class="form-control" /> < span asp-validation-for="Title" class="text-danger">< /span> < /div> < div class="form-group"> < label asp-for="ReleaseDate" class="control-label">< /label> < input asp-for="ReleaseDate" class="form-control" /> < span asp-validation-for="ReleaseDate" class="text-danger">< /span> < /div> < div class="form-group"> < label asp-for="Genre" class="control-label">< /label> < input asp-for="Genre" class="form-control" /> < span asp-validation-for="Genre" class="text-danger">< /span> < /div> < div class="form-group"> < label asp-for="Price" class="control-label">< /label> < input asp-for="Price" class="form-control" /> < span asp-validation-for="Price" class="text-danger">< /span> < /div> < div class="form-group"> < input type="submit" value="Create" class="btn btn-primary" /> < /div> < /form> < /div> < /div> < div> < a asp-action="Index">Back to List < /div> @{ // ここでエラー } @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }
「Cannot resolve section 'Scripts'」と出ます。
また、エラーの個所をコメントアウトして実行しても、ボタンが動作しません orz
どうしてこうなった/(^o^)\
エラーを何とかする
とりあえずエラーを直します。
やるべきことは、「@section Scripts{}」の削除です。
・・・まじかよ。。。
Create.cshtml
~省略~ @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
これで問題なく動作します。
また、そもそも上記が不要なら削除してしまっても OK です。
ボタンなどが動くようにする
ボタンなどが動作しない原因は、 TagHelper が無いためです。
ということで、先頭に addTagHelper を追加します。
Create.cshtml
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers ~省略~
TagHelper については他の画面も同様であるため、同じように追加します。
Visual Studio でスキャフォールドしたときも同じような状態になるのでしょうか。
それともみんな使ってなry
ルーティング
あ、あとここまでの設定だと生成された MovieController のルーティングが効かず、真っ白な画面が表示されてしまうため、この辺で試したように 設定しておきます。
MovieController.cs
using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using EfCoreSample.Models; namespace EfCoreSample.Controllers { public class MoviesController : Controller { private readonly EfCoreSampleContext _context; public MoviesController(EfCoreSampleContext context) { _context = context; } [Route("/")] [Route("/Movies")] // GET: Movies public async TaskIndex() { return View(await _context.Movie.ToListAsync()); } [Route("/Movies/Details/{*id}")] public async Task Details(int? id) { if (id == null) { return NotFound(); } var movie = await _context.Movie .FirstOrDefaultAsync(m => m.MovieId == id); if (movie == null) { return NotFound(); } return View(movie); } [Route("/Movies/Create")] public IActionResult Create() { return View("/Views/Movies/Create.cshtml"); } [HttpPost] [Route("/Movies/Create")] [ValidateAntiForgeryToken] public async Task Create([Bind("MovieId,Title,ReleaseDate,Genre,Price")] Movie movie) { if (ModelState.IsValid) { _context.Add(movie); await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } return View(movie); } [Route("/Movies/Edit/{*id}")] public async Task Edit(int? id) { if (id == null) { return NotFound(); } var movie = await _context.Movie.FindAsync(id); if (movie == null) { return NotFound(); } return View(movie); } [HttpPost] [Route("/Movies/Edit/{*id}")] [ValidateAntiForgeryToken] public async Task Edit(int id, [Bind("MovieId,Title,ReleaseDate,Genre,Price")] Movie movie) { if (id != movie.MovieId) { return NotFound(); } if (ModelState.IsValid) { try { _context.Update(movie); await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!MovieExists(movie.MovieId)) { return NotFound(); } else { throw; } } return RedirectToAction(nameof(Index)); } return View(movie); } [Route("/Movies/Delete/{*id}")] public async Task Delete(int? id) { if (id == null) { return NotFound(); } var movie = await _context.Movie .FirstOrDefaultAsync(m => m.MovieId == id); if (movie == null) { return NotFound(); } return View(movie); } [HttpPost, ActionName("Delete")] [Route("/Movies/Delete/{*id}")] [ValidateAntiForgeryToken] public async Task DeleteConfirmed(int id) { var movie = await _context.Movie.FindAsync(id); _context.Movie.Remove(movie); await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } private bool MovieExists(int id) { return _context.Movie.Any(e => e.MovieId == id); } } }
DB のマイグレーション
CSS などが無いため非常に簡素ではあるものの、 CRUD ができそうなページが出来上がりました。
が、そのまま実行すると DB は生成されますが、テーブルが見つからないとエラーになります。
DB にテーブルを追加するには、マイグレーションを実行します。
dotnet ef migrations add AddMovie dotnet ef database update
CUI は GUI より最初とっつきにくい印象があるものの覚えた後は楽なので(個人の感想です)、どちらで慣れるのが良いのかは決めづらいどころですが。
これでとりあえず CRUD ができるようになりました。
次回はもう少し生成されたものの中身を見てみたいと思います。
参照
【C#】Task の話 その 5
はじめに
今回は例外について。
async void と async Task
async/await に関連するアンチパターンの一つに、 「async void を使う」というものがあります。
前回取り上げた await は、戻り値が awaitable (例: TaskAwaiter) である必要があるため、 async void の場合 await できません。
これに起因して、結果がわからないから禁止、という内容となります。
…なんですが、どうしても一番の呼び出し元は async void になる場合があるよねぇ、とも思っていました。
特に Unity の場合、(今だと Job System なるものがあるというので状況が違うかもしれませんが) UI のイベントも同期的なものとして返ってきます。
そこから async/await で処理を実行しようとすれば、 async void を使うことになります。
では、 async void だとどのようにマズいのかを見てみることにします。
※とはいいつつググるといくらでも出てくるんですけど。
- neue cc - asyncの落とし穴Part2, SynchronizationContextの向こう側
- neue cc - asyncの落とし穴Part3, async voidを避けるべき100億の理由
Exception がキャッチできない
結果が受け取られないということに類似して、 async void だと Exception がキャッチできない、という問題があります。
Program.cs
using System; using System.Threading.Tasks; namespace TaskSample { class Program { static async Task Main(string[] args) { var player = new TaskSamplePlayer(); // 呼び出し先で async/await 使用. // 呼び出し元も await 使用. try { await player.PlayAsync(); } catch (Exception e) { Console.WriteLine("[1] " + e.Message); } // 呼び出し先では async/await を使用しない. // 呼び出し元では await 使用. try { await player.Play(); } catch (Exception e) { Console.WriteLine("[2] " + e.Message); } // 呼び出し先では async/await を使用しない. // 呼び出し元でも await を使用しない. try { player.Play(); } catch (Exception e) { Console.WriteLine("[3] " + e.Message); } // 呼び出し先で async void を使用. // void なので呼び出し元では await できない. try { player.PlayVoidAsync(); } catch (Exception e) { Console.WriteLine("[4] " + e.Message); } Console.WriteLine("end"); } } }
TaskSamplePlayer.cs
using System; using System.Threading.Tasks; namespace TaskSample { public class TaskSamplePlayer { public async Task PlayAsync() { await Task.Run(() => { throw new Exception("Error: PlayAsync"); }); } public Task Play() { return Task.Run(() => { throw new Exception("Error: Play"); }); } public async void PlayVoidAsync() { await Task.Run(() => { throw new Exception("Error: PlayVoidAsync"); }); } } }
結果は下記の通りです。
[1] Error: PlayAsync [2] Error: Play end
await してないと try - catch で Exception がキャッチできない、という結果となりました。
理由はいつものごとく?メインスレッドのでの try - catch の処理が先に終わってしまい、後からワーカースレッドで例外が発生する状態になるからですね。
これを考えると、あるメソッドで async void を返す場合、そのメソッドを呼び出すメソッドではなく、自分自身でエラー処理の責務を負う必要があることになります。
複数の Task を実行する
async/await の Exception を調べる中で面白いものを見つけたので。
複数の Task を連続・または同時に実行する場合、その待ち方によって結果が変わる場合があります。
Program.cs
using System; using System.Threading.Tasks; namespace TaskSample { class Program { static async Task Main(string[] args) { var player = new TaskSamplePlayer(); Console.WriteLine("1"); // await try { await player.PlayAsync(); await player.PlayAsync2(); } catch (Exception e) { Console.WriteLine("[1] " + e.GetType() + " [Message] " + e.Message); } Console.WriteLine("2"); // Wait() try { player.PlayAsync().Wait(); player.PlayAsync2().Wait(); } catch (Exception e) { Console.WriteLine("[2] " + e.GetType() + " [Message] " + e.Message); } Console.WriteLine("3"); // WaitAll() try { var p1 = player.PlayAsync(); var p2 = player.PlayAsync2(); Task.WaitAll(p1, p2); } catch (Exception e) { Console.WriteLine("[3] " + e.GetType() + " [Message] " + e.Message); } Console.WriteLine("4"); // await WhenAll() try { var p1 = player.PlayAsync(); var p2 = player.PlayAsync2(); await Task.WhenAll(p1, p2); } catch (Exception e) { Console.WriteLine("[4] " + e.GetType() + " [Message] " + e.Message); } Console.WriteLine("end"); } } }
TaskSamplePlayer.cs
using System; using System.Threading.Tasks; namespace TaskSample { public class TaskSamplePlayer { public async Task PlayAsync() { Console.WriteLine("Start PlayAsync"); await Task.Run(() => { throw new ArgumentException("Error: PlayAsync"); }); } public async Task PlayAsync2() { Console.WriteLine("Start PlayAsync2"); await Task.Run(() => { throw new InvalidOperationException("Error: PlayAsync2"); }); } } }
2 つの Task を見分けるため、 Exception の内容を変えたりしていますが、内容は変わらないはずです。
で、結果はこうなります。
1 Start PlayAsync [1] System.ArgumentException [Message] Error: PlayAsync 2 Start PlayAsync [2] System.AggregateException [Message] One or more errors occurred. (Error: Pla yAsync) 3 Start PlayAsync Start PlayAsync2 [3] System.AggregateException [Message] One or more errors occurred. (Error: Pla yAsync) (Error: PlayAsync2) 4 Start PlayAsync Start PlayAsync2 [4] System.ArgumentException [Message] Error: PlayAsync end
わかりづらいので説明を追加すると
1. await を使って Task をそれぞれ待った場合
2 つの Task の内最初のものだけが実行され、 Task 内で発生した ArgumentException が返っています。
2. Wait() を使って Task をそれぞれ待った場合
2 つの Task の内最初のものだけが実行され、 AggregateException が返っています。
3. WaitAll() を使って Task をまとめて待った場合
両方の Task が実行され、 AggregateException が返っています。
Message には両方の内容が含まれています。
4. await WhenAll() を使って Task をまとめて待った場合
両方の Task が実行され、 Message には両方の内容が含まれますが、 Exception の Type は最初の Task のものだけが含まれています。
Exception の投げ方
今回のサンプルでは 全部 Exception でキャッチする、という雑いやり方をしていたためキャッチできていましたが、ちゃんと Exception の種類ごとに受け取って適切な処理をしようとすると、待ち方にも気を配る必要がありますね。
どこからこの違いは生まれるのでしょうか。
ということでコードを追ってみますよ。
1. await を使って Task をそれぞれ待った場合
await ということで前回登場した TaskAwaiter かな?とも思ったのですが、名前通り Task の完了を待ち受けているだけで、関連しそうなコードは見つかりませんでした。
ということで Task を探ってみます。
なお Task には限らないと思いますが、基本的に Exception は ThrowHelper というクラスが一括で投げているようです。
例えば Task の InternalStartNew ではこのようにしています。
~省略~ if (scheduler == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.scheduler); ~省略~
ThrowHelper の中身を見ると、大量の throw new XXXException と、 ExceptionArgument(enum) などが定義されていました。
ではその 2 も参考にコードを読み進めてみますよ。
ふむふむ。
特に変わったところはないな。。。
。。。
そりゃそうですね。 Task を一つだけ実行して、その中で発生した Exception がそのまま返ってるんだから orz
と、というわけで気を取り直して、 Wait() を見てみますよ。
2. Wait() を使って Task をそれぞれ待った場合
では Wait() ですね。
Wait() もオーバーロードされており、デフォルト値を入れて下記が呼ばれます。
Task.cs
~省略~ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken) { ~省略~ if (IsWaitNotificationEnabledOrNotRanToCompletion) // avoid a few unnecessary volatile reads if we completed successfully { // Notify the debugger of the wait completion if it's requested such a notification NotifyDebuggerOfWaitCompletionIfNecessary(); // If cancellation was requested and the task was canceled, throw an // OperationCanceledException. This is prioritized ahead of the ThrowIfExceptional // call to bring more determinism to cases where the same token is used to // cancel the Wait and to cancel the Task. Otherwise, there's a race condition between // whether the Wait or the Task observes the cancellation request first, // and different exceptions result from the different cases. if (IsCanceled) cancellationToken.ThrowIfCancellationRequested(); // If an exception occurred, or the task was cancelled, throw an exception. ThrowIfExceptional(true); } Debug.Assert((m_stateFlags & TASK_STATE_FAULTED) == 0, "Task.Wait() completing when in Faulted state."); return true; } ~省略~
どうやら関係がありそうなのは ThrowIfExceptional のようです。
Task.cs
~省略~ internal void ThrowIfExceptional(bool includeTaskCanceledExceptions) { Debug.Assert(IsCompleted, "ThrowIfExceptional(): Expected IsCompleted == true"); Exception exception = GetExceptions(includeTaskCanceledExceptions); if (exception != null) { UpdateExceptionObservedStatus(); throw exception; } } ~省略~
この GetExceptions で AggregateException が返されています。
あとここでは返ってきた AggregateException をそのまま投げていますね。
3. WaitAll() を使って Task をまとめて待った場合
こちらは WaitAllCore() から AggregateException を投げているようです。
Task.cs
~省略~ private static bool WaitAllCore(Task[] tasks, int millisecondsTimeout, CancellationToken cancellationToken) { ~省略~ if (returnValue && (exceptionSeen || cancellationSeen)) { // If the WaitAll was canceled and tasks were canceled but not faulted, // prioritize throwing an OCE for canceling the WaitAll over throwing an // AggregateException for all of the canceled Tasks. This helps // to bring determinism to an otherwise non-determistic case of using // the same token to cancel both the WaitAll and the Tasks. if (!exceptionSeen) cancellationToken.ThrowIfCancellationRequested(); // Now gather up and throw all of the exceptions. foreach (var task in tasks) AddExceptionsForCompletedTask(ref exceptions, task); Debug.Assert(exceptions != null, "Should have seen at least one exception"); ThrowHelper.ThrowAggregateException(exceptions); } return returnValue; } ~省略~
この辺りの扱いの違いは、どこから来ているのでしょうか。
気になるところではありますが、終わりそうにない気がするのでこの辺に留めておきます。
4. await WhenAll() を使って Task をまとめて待った場合
個人的にはこの結果が一番不思議な感じがします。
AggregateException が返るわけではないけど Message はまとめられているという。
Task.cs
~省略~ public static Task WhenAll(params Task[] tasks) { ~省略~ Task[] tasksCopy = new Task[taskCount]; for (int i = 0; i < taskCount; i++) { Task task = tasks[i]; if (task == null) ThrowHelper.ThrowArgumentException(ExceptionResource.Task_MultiTaskContinuation_NullTask, ExceptionArgument.tasks); tasksCopy[i] = task; } // The rest can be delegated to InternalWhenAll() return InternalWhenAll(tasksCopy); } ~省略~
Task.cs
~省略~ private static Task InternalWhenAll(Task[] tasks) { Debug.Assert(tasks != null, "Expected a non-null tasks array"); return (tasks.Length == 0) ? // take shortcut if there are no tasks upon which to wait Task.CompletedTask : new WhenAllPromise(tasks); } ~省略~
tasks の要素数は 0 ではない( 2 )ため、 WhenAllPromise のインスタンスが返されます。
Task.cs
~省略~ private sealed class WhenAllPromise : Task, ITaskCompletionAction { ~省略~ internal WhenAllPromise(Task[] tasks) : base() { ~省略~ m_tasks = tasks; m_count = tasks.Length; foreach (var task in tasks) { if (task.IsCompleted) this.Invoke(task); // short-circuit the completion action, if possible else task.AddCompletionAction(this); // simple completion action } } public void Invoke(Task completedTask) { ~省略~ // Decrement the count, and only continue to complete the promise if we're the last one. if (Interlocked.Decrement(ref m_count) == 0) { // Set up some accounting variables List observedExceptions = null; Task canceledTask = null; // Loop through antecedents: // If any one of them faults, the result will be faulted // If none fault, but at least one is canceled, the result will be canceled // If none fault or are canceled, then result will be RanToCompletion for (int i = 0; i < m_tasks.Length; i++) { var task = m_tasks[i]; Debug.Assert(task != null, "Constituent task in WhenAll should never be null"); if (task.IsFaulted) { if (observedExceptions == null) observedExceptions = new List (); observedExceptions.AddRange(task.GetExceptionDispatchInfos()); } else if (task.IsCanceled) { if (canceledTask == null) canceledTask = task; // use the first task that's canceled } // Regardless of completion state, if the task has its debug bit set, transfer it to the // WhenAll task. We must do this before we complete the task. if (task.IsWaitNotificationEnabled) this.SetNotificationForWaitCompletion(enabled: true); else m_tasks[i] = null; // avoid holding onto tasks unnecessarily } if (observedExceptions != null) { Debug.Assert(observedExceptions.Count > 0, "Expected at least one exception"); //We don't need to TraceOperationCompleted here because TrySetException will call Finish and we'll log it there TrySetException(observedExceptions); } ~省略~ } Debug.Assert(m_count >= 0, "Count should never go below 0"); } ~省略~ } ~省略~
待つ対象となっている Task を一つずつ実行し、失敗したら ExceptionDispatchInfo (発生した Exception の保持やその Exception を投げたりするようです)に追加し、 1 つ以上 Exception があれば TrySetException() に渡しているようです。
で、ここから若干ややこしいのですが、この TrySetException() というのは Task< TResult> クラスで定義されているのですが、このクラスは Future.cs にあります。
Future.cs
~省略~ internal bool TrySetException(object exceptionObject) { ~省略~ bool returnValue = false; // "Reserve" the completion for this task, while making sure that: (1) No prior reservation // has been made, (2) The result has not already been set, (3) An exception has not previously // been recorded, and (4) Cancellation has not been requested. // // If the reservation is successful, then add the exception(s) and finish completion processing. // // The lazy initialization may not be strictly necessary, but I'd like to keep it here // anyway. Some downstream logic may depend upon an inflated m_contingentProperties. EnsureContingentPropertiesInitialized(); if (AtomicStateUpdate(TASK_STATE_COMPLETION_RESERVED, TASK_STATE_COMPLETION_RESERVED | TASK_STATE_RAN_TO_COMPLETION | TASK_STATE_FAULTED | TASK_STATE_CANCELED)) { AddException(exceptionObject); // handles singleton exception or exception collection Finish(false); returnValue = true; } return returnValue; } ~省略~
ここでは AddException() で ExceptionDispatchInfo を追加していますが、このメソッドは Task クラスにあります。
AddException() もオーバーロードがあり、第二引数に false を設定して下記を呼びます。
Task.cs
~省略~ internal void AddException(object exceptionObject, bool representsCancellation) { ~省略~ // Lazily initialize the holder, ensuring only one thread wins. var props = EnsureContingentPropertiesInitialized(); if (props.m_exceptionsHolder == null) { TaskExceptionHolder holder = new TaskExceptionHolder(this); if (Interlocked.CompareExchange(ref props.m_exceptionsHolder, holder, null) != null) { // If someone else already set the value, suppress finalization. holder.MarkAsHandled(false); } } lock (props) { props.m_exceptionsHolder.Add(exceptionObject, representsCancellation); } } ~省略~
ここでは TaskExceptionHolder に ExceptionDispatchInfo を追加しています。
Add() では第二引数の値が false のため AddFaultException() を呼んでいます。
TaskExceptionHolder.cs
~省略~ private void AddFaultException(object exceptionObject) { Debug.Assert(exceptionObject != null, "AddFaultException(): Expected a non-null exceptionObject"); // Initialize the exceptions list if necessary. The list should be non-null iff it contains exceptions. var exceptions = m_faultExceptions; if (exceptions == null) m_faultExceptions = exceptions = new List(1); else Debug.Assert(exceptions.Count > 0, "Expected existing exceptions list to have > 0 exceptions."); // Handle Exception by capturing it into an ExceptionDispatchInfo and storing that if (exceptionObject is Exception exception) { exceptions.Add(ExceptionDispatchInfo.Capture(exception)); } else { // Handle ExceptionDispatchInfo by storing it into the list if (exceptionObject is ExceptionDispatchInfo edi) { exceptions.Add(edi); } else { // Handle enumerables of exceptions by capturing each of the contained exceptions into an EDI and storing it if (exceptionObject is IEnumerable exColl) { ~省略~ foreach (var exc in exColl) { ~省略~ exceptions.Add(ExceptionDispatchInfo.Capture(exc)); } ~省略~ } else { // Handle enumerables of EDIs by storing them directly if (exceptionObject is IEnumerable ediColl) { exceptions.AddRange(ediColl); ~省略~ } // Anything else is a programming error else { throw new ArgumentException(SR.TaskExceptionHolder_UnknownExceptionType, nameof(exceptionObject)); } } } } if (exceptions.Count > 0) MarkAsUnhandled(); } ~省略~
ここでは ExceptionDispatchInfo のリストに値を追加しています。
で、先ほどから出てきている MarkAsHandled() 、 MarkAsUnhandled() というのは GC のファイナライズを実行する・しないを指定しているようです。
ファイナライズで何が実行されるかというと
TaskExceptionHolder.cs
~省略~ ~TaskExceptionHolder() { if (m_faultExceptions != null && !m_isHandled) { // We will only propagate if this is truly unhandled. The reason this could // ever occur is somewhat subtle: if a Task's exceptions are observed in some // other finalizer, and the Task was finalized before the holder, the holder // will have been marked as handled before even getting here. // Publish the unobserved exception and allow users to observe it AggregateException exceptionToThrow = new AggregateException( SR.TaskExceptionHolder_UnhandledException, m_faultExceptions); UnobservedTaskExceptionEventArgs ueea = new UnobservedTaskExceptionEventArgs(exceptionToThrow); TaskScheduler.PublishUnobservedTaskException(m_task, ueea); } } ~省略~
TaskScheduler.PublishUnobservedTaskException() で何をしているかというと、 UnobservedTaskExceptionEventArgs というイベントを実行しています。
ではこの UnobservedTaskException とは何かというと、 Task 内で発生し、処理がされないままの Exception (無視された Exception) をまとめて Exception として投げるためのイベント、ということのようです。
- UnobservedTaskExceptionEventArgs(AggregateException) Constructor (System.Threading.Tasks) - Microsoft Docs
- TaskScheduler.UnobservedTaskException Event (System.Threading.Tasks) - Microsoft Docs
この辺りの Exception の扱いの違いが、結果の違いとなって現れているようですね。
AggregateException とは
Wait() や WaitAll() で使われていた、 AggregateException について少しだけ。
Microsoft Docsによると、アプリケーションの実行中に発生した、 1 つ以上のエラーを表す、とのこと。
この辺りの説明を読むと、 複数の Task がそれぞれ非同期で Exception を出しうる場合に、それらを一括で扱えるようにするために AggregateException が使われている、ということのようです。
今回の例では Wait() は単体の Task で使いましたが、親子関係を持たせた場合にこれが必要になるようです。
await では親子関係は持てないの?という疑問はありますが、この辺りは次回以降試してみたいと思います。
なお、 AggregateException が返る場合、 InnerExceptions で個別の Exception を取り出せたり、 AggregateException に親子関係がある場合も Flatten() を使ってまとめて取り出せたりするため便利ですね。
ということで雑に Exception でキャッチせず、ちゃんと AggregateException を指定しましょう(戒め)。
参照
- dotnet/coreclr - GitHub
- スラスラわかる C#
- 非同期理解のためにasync/awaitとTaskの基礎を学んだ話 - Qiita
- Taskを極めろ!async/await完全攻略 - Qiita
- neue cc - asyncの落とし穴Part2, SynchronizationContextの向こう側
- neue cc - asyncの落とし穴Part3, async voidを避けるべき100億の理由
AggregateException
- AggregateException Class (System) - Microsoft Docs
- TPL入門 (12) - タスクの例外処理 - xin9le.net
- 非同期処理とAggregateException - プログラミングな日々
UnobservedTaskException
【C#】Task の話 その 4
はじめに
さて今回は、 async/await の話。
async/await
その 1 で登場したこのコード。
そのままだとメインスレッドの処理が先に終わってしまい、 Task.Run の中身が実行される前に処理が完了してしまう、という話を書きました。
Program.cs
using System; namespace TaskSample { class Program { static void Main(string[] args) { var player = new TaskSamplePlayer(); player.Play(); } } }
TaskSamplePlayer.cs
using System; using System.Threading; using System.Threading.Tasks; namespace TaskSample { public class TaskSamplePlayer { public void Play() { Task.Run(() => { Console.WriteLine("Hello world!"); }); } } }
この程度の処理であれば Thread.Sleep などで少し待ってやれば OK な気もします。
ただ、もっと重い処理を別スレッドで行いたい場合、待ち時間より処理の時間がかかると先ほどと同じ結果が起こります。
これを処理が完了するのを待ってメインスレッドの処理が終わるようにする、という方法にはいくつかあり、その一つが async/await です。
※下記のコードは C# 7.1 以上で実行する必要があります。
Program.cs
using System; using System.Threading.Tasks; namespace TaskSample { class Program { static async Task Main(string[] args) { var player = new TaskSamplePlayer(); await player.PlayAsync(); // player.Play() の処理が完了した後に実行される. Console.WriteLine("end"); } } }
TaskSamplePlayer.cs
using System; using System.Threading; using System.Threading.Tasks; namespace TaskSample { public class TaskSamplePlayer { public async Task PlayAsync() { await Task.Run(() => { Console.WriteLine("Hello world!"); }); } } }
C# 7.1 から Main メソッドに async キーワードを付けられるようになりました。
これでシンプルに PlayAsync() の処理を待つことができます。
IL で比較する
スラスラわかる C# などによると、 async/await の内 async キーワード自体には何か機能があるわけではなく、 await を下位互換性を保ちながら導入するために必要なもの、という位置づけのようです。
ということは、 await を追いかけるとどのようなことをしているかがわかりそうです。
これを確かめるため、 ReSharper 先生の IL Viewer に力を借りることにします。
元のコード (TaskSamplePlayer.cs)
using System; using System.Threading.Tasks; namespace TaskSample { public class TaskSamplePlayer { public async Task PlayAsync() { await Task.Run(() => { Console.WriteLine("hello"); }); } public Task Play() { return Task.Run(() => { Console.WriteLine("hello");}); } } }
これの PlayAsync() と Play() を比較してみます。
PlayAsync()
// Type: TaskSample.TaskSamplePlayer // Assembly: TaskSample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null // MVID: XXXX // Location: C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.dll // Sequence point data from C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.pdb .class public auto ansi beforefieldinit TaskSample.TaskSamplePlayer extends [System.Runtime]System.Object { .class nested private sealed auto ansi serializable beforefieldinit '<>c' extends [System.Runtime]System.Object { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .field public static initonly class TaskSample.TaskSamplePlayer/'<>c' '<>9' .field public static class [System.Runtime]System.Action '<>9__0_0' .field public static class [System.Runtime]System.Action '<>9__1_0' ~省略~ .method assembly hidebysig instance void 'b__0_0'() cil managed { .maxstack 8 // [10 34 - 10 35] IL_0000: nop // [10 36 - 10 63] IL_0001: ldstr "hello" IL_0006: call void [System.Console]System.Console::WriteLine(string) IL_000b: nop // [10 64 - 10 65] IL_000c: ret } // end of method '<>c'::' b__0_0' ~省略~ } // end of class '<>c' .class nested private sealed auto ansi beforefieldinit ' d__0' extends [System.Runtime]System.Object implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .field public int32 '<>1__state' .field public valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder '<>t__builder' .field public class TaskSample.TaskSamplePlayer '<>4__this' .field private valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter '<>u__1' .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 IL_0000: ldarg.0 // this IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method ' d__0'::.ctor .method private final hidebysig virtual newslot instance void MoveNext() cil managed { .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext() .maxstack 3 .locals init ( [0] int32 V_0, [1] valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter V_1, [2] class TaskSample.TaskSamplePlayer/' d__0' V_2, [3] class [System.Runtime]System.Exception V_3 ) IL_0000: ldarg.0 // this IL_0001: ldfld int32 TaskSample.TaskSamplePlayer/' d__0'::'<>1__state' IL_0006: stloc.0 // V_0 .try { IL_0007: ldloc.0 // V_0 IL_0008: brfalse.s IL_000c IL_000a: br.s IL_000e IL_000c: br.s IL_0066 // [9 9 - 9 10] IL_000e: nop // [10 13 - 10 67] IL_000f: ldsfld class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__0_0' IL_0014: dup IL_0015: brtrue.s IL_002e IL_0017: pop IL_0018: ldsfld class TaskSample.TaskSamplePlayer/'<>c' TaskSample.TaskSamplePlayer/'<>c'::'<>9' IL_001d: ldftn instance void TaskSample.TaskSamplePlayer/'<>c'::' b__0_0'() IL_0023: newobj instance void [System.Runtime]System.Action::.ctor(object, native int) IL_0028: dup IL_0029: stsfld class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__0_0' IL_002e: call class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Threading.Tasks.Task::Run(class [System.Runtime]System.Action) IL_0033: callvirt instance valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter [System.Runtime]System.Threading.Tasks.Task::GetAwaiter() IL_0038: stloc.1 // V_1 IL_0039: ldloca.s V_1 IL_003b: call instance bool [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted() IL_0040: brtrue.s IL_0082 IL_0042: ldarg.0 // this IL_0043: ldc.i4.0 IL_0044: dup IL_0045: stloc.0 // V_0 IL_0046: stfld int32 TaskSample.TaskSamplePlayer/' d__0'::'<>1__state' IL_004b: ldarg.0 // this IL_004c: ldloc.1 // V_1 IL_004d: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter TaskSample.TaskSamplePlayer/' d__0'::'<>u__1' IL_0052: ldarg.0 // this IL_0053: stloc.2 // V_2 IL_0054: ldarg.0 // this IL_0055: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/' d__0'::'<>t__builder' IL_005a: ldloca.s V_1 IL_005c: ldloca.s V_2 IL_005e: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::AwaitUnsafeOnCompleted d__0'>(!!0/*valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter*/&, !!1/*class TaskSample.TaskSamplePlayer/' d__0'*/&) IL_0063: nop IL_0064: leave.s IL_00b8 IL_0066: ldarg.0 // this IL_0067: ldfld valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter TaskSample.TaskSamplePlayer/' d__0'::'<>u__1' IL_006c: stloc.1 // V_1 IL_006d: ldarg.0 // this IL_006e: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter TaskSample.TaskSamplePlayer/' d__0'::'<>u__1' IL_0073: initobj [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter IL_0079: ldarg.0 // this IL_007a: ldc.i4.m1 IL_007b: dup IL_007c: stloc.0 // V_0 IL_007d: stfld int32 TaskSample.TaskSamplePlayer/' d__0'::'<>1__state' IL_0082: ldloca.s V_1 IL_0084: call instance void [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::GetResult() IL_0089: nop IL_008a: leave.s IL_00a4 } // end of .try catch [System.Runtime]System.Exception { IL_008c: stloc.3 // V_3 IL_008d: ldarg.0 // this IL_008e: ldc.i4.s -2 // 0xfe IL_0090: stfld int32 TaskSample.TaskSamplePlayer/' d__0'::'<>1__state' IL_0095: ldarg.0 // this IL_0096: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/' d__0'::'<>t__builder' IL_009b: ldloc.3 // V_3 IL_009c: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::SetException(class [System.Runtime]System.Exception) IL_00a1: nop IL_00a2: leave.s IL_00b8 } // end of catch // [11 9 - 11 10] IL_00a4: ldarg.0 // this IL_00a5: ldc.i4.s -2 // 0xfe IL_00a7: stfld int32 TaskSample.TaskSamplePlayer/' d__0'::'<>1__state' IL_00ac: ldarg.0 // this IL_00ad: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/' d__0'::'<>t__builder' IL_00b2: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::SetResult() IL_00b7: nop IL_00b8: ret } // end of method ' d__0'::MoveNext .method private final hidebysig virtual newslot instance void SetStateMachine( class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine ) cil managed { .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = (01 00 00 00 ) .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine(class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine) .maxstack 8 IL_0000: ret } // end of method ' d__0'::SetStateMachine } // end of class ' d__0' .method public hidebysig instance class [System.Runtime]System.Threading.Tasks.Task PlayAsync() cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type) = ( 01 00 2b 54 61 73 6b 53 61 6d 70 6c 65 2e 54 61 // ..+TaskSample.Ta 73 6b 53 61 6d 70 6c 65 50 6c 61 79 65 72 2b 3c // skSamplePlayer+< 50 6c 61 79 41 73 79 6e 63 3e 64 5f 5f 30 00 00 // PlayAsync>d__0.. ) // type(class TaskSample.TaskSamplePlayer/' d__0') .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (01 00 00 00 ) .maxstack 2 .locals init ( [0] class TaskSample.TaskSamplePlayer/' d__0' V_0, [1] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder V_1 ) IL_0000: newobj instance void TaskSample.TaskSamplePlayer/' d__0'::.ctor() IL_0005: stloc.0 // V_0 IL_0006: ldloc.0 // V_0 IL_0007: ldarg.0 // this IL_0008: stfld class TaskSample.TaskSamplePlayer TaskSample.TaskSamplePlayer/' d__0'::'<>4__this' IL_000d: ldloc.0 // V_0 IL_000e: call valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create() IL_0013: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/' d__0'::'<>t__builder' IL_0018: ldloc.0 // V_0 IL_0019: ldc.i4.m1 IL_001a: stfld int32 TaskSample.TaskSamplePlayer/' d__0'::'<>1__state' IL_001f: ldloc.0 // V_0 IL_0020: ldfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/' d__0'::'<>t__builder' IL_0025: stloc.1 // V_1 IL_0026: ldloca.s V_1 IL_0028: ldloca.s V_0 IL_002a: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start d__0'>(!!0/*class TaskSample.TaskSamplePlayer/' d__0'*/&) IL_002f: ldloc.0 // V_0 IL_0030: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/' d__0'::'<>t__builder' IL_0035: call instance class [System.Runtime]System.Threading.Tasks.Task [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task() IL_003a: ret } // end of method TaskSamplePlayer::PlayAsync ~省略~ } // end of class TaskSample.TaskSamplePlayer
Play()
// Type: TaskSample.TaskSamplePlayer // Assembly: TaskSample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null // MVID: XXXX // Location: C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.dll // Sequence point data from C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.pdb .class public auto ansi beforefieldinit TaskSample.TaskSamplePlayer extends [System.Runtime]System.Object { .class nested private sealed auto ansi serializable beforefieldinit '<>c' extends [System.Runtime]System.Object { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .field public static initonly class TaskSample.TaskSamplePlayer/'<>c' '<>9' .field public static class [System.Runtime]System.Action '<>9__0_0' .field public static class [System.Runtime]System.Action '<>9__1_0' ~省略~ .method assembly hidebysig instance void 'b__1_0'() cil managed { .maxstack 8 // [15 35 - 15 36] IL_0000: nop // [15 37 - 16 26] IL_0001: ldstr "hello" IL_0006: call void [System.Console]System.Console::WriteLine(string) IL_000b: nop // [16 26 - 16 27] IL_000c: ret } // end of method '<>c'::' b__1_0' } // end of class '<>c' ~省略~ .method public hidebysig instance class [System.Runtime]System.Threading.Tasks.Task Play() cil managed { .maxstack 2 .locals init ( [0] class [System.Runtime]System.Threading.Tasks.Task V_0 ) // [14 9 - 14 10] IL_0000: nop // [15 13 - 16 29] IL_0001: ldsfld class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__1_0' IL_0006: dup IL_0007: brtrue.s IL_0020 IL_0009: pop IL_000a: ldsfld class TaskSample.TaskSamplePlayer/'<>c' TaskSample.TaskSamplePlayer/'<>c'::'<>9' IL_000f: ldftn instance void TaskSample.TaskSamplePlayer/'<>c'::' b__1_0'() IL_0015: newobj instance void [System.Runtime]System.Action::.ctor(object, native int) IL_001a: dup IL_001b: stsfld class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__1_0' IL_0020: call class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Threading.Tasks.Task::Run(class [System.Runtime]System.Action) IL_0025: stloc.0 // V_0 IL_0026: br.s IL_0028 // [17 9 - 17 10] IL_0028: ldloc.0 // V_0 IL_0029: ret } // end of method TaskSamplePlayer::Play ~省略~ } // end of class TaskSample.TaskSamplePlayer
PlayAsync() の圧倒的長さ。。。
特に気になるのはこの辺り。
- System.Runtime.CompilerServices.TaskAwaiter
- System.Runtime.CompilerServices.IAsyncStateMachine
- System.Runtime.CompilerServices.AsyncTaskMethodBuilder
- try - catch
上記のキーワードでググると有益な情報がどかどか出てくるので、これ僕が何か書く必要ないやん。。。てなります。
(ありがとうございます)
なお私も ILSpy を使ってみようかと思ったのですが、オプションから async メソッドをデコンパイルしない、というオプションが見つからなかったため断念しました。
GitHub の Isssue を見てると C# の複数バージョンに対応するため?に省かれたっポイですが。
- 第3回 非同期メソッドの内部実装とAwaitableパターンの独自実装 (1/2):連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門(最終回) - @IT
- 非同期メソッドの内部実装 - C# によるプログラミング入門 - ++C++; // 未確認飛行 C
- 非同期メソッド入門 (7) - 内部実装を覗く - xin9le.net
なので細かいところはそちらを参照、というところなのですが、乱雑にまとめますと
- await を使うと IAsyncStateMachine を継承した構造体が作られる
(リンク先を見ると StateMachine という名前らしいです) - 非同期メソッドが完了したかなどの状態は TaskAwaiter が持つ
- 1.が AsyncTaskMethodBuilder を使った非同期メソッドの生成や 2.の登録・監視などを行う
といった感じでしょうか。
TaskAwaiter はコードを見ると interface を継承していますが、中身は空のようでした。
また、 await で処理を待ち受けることができる型は、 Awaitable パターンに沿っていること、という制限があるようです。
この Awaitable パターンというのは特定の型を指すのではなく、 GetAwaiter() など処理に必要なメソッドが実装されていれば良い、ということのようです。
動的な型を持つ言語や Go の interface もこんな感じでしたね。
この辺りで発生するエラーをキャッチするために try - catch があるのかもしれませんね。
IL のコードを載せたために長くなってしまったので、ここで切ります。
参照
- dotnet/coreclr - GitHub
- スラスラわかる C#
- C# 7 Series, Part 2: Async Main – Mark Zhou's Tech Blog
- 非同期メソッドの内部実装 - C# によるプログラミング入門 - ++C++; // 未確認飛行 C
- 非同期メソッド入門 (7) - 内部実装を覗く - xin9le.net
- await可能なクラスを作ってみよう - かずきのBlog@hatena
- 第3回 非同期メソッドの内部実装とAwaitableパターンの独自実装 (1/2):連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門(最終回) - @IT