vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

Entity Framework Core で色々な SQL を投げてみる 2

はじめに

SELECT 文の続きです。

今回も SQL ゼロからはじめるデータベース操作 を参考に、色々試してみますよ。

SELECT 2

コード

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% にこだわるなら、もう少し別の方法もあるかもしれませんね。

[参照]

コード

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(DbContextOptions options)
            : 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

はじめに

さて、 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 はローカルにインストールしたものを使用しています。

https://www.pgadmin.org/

接続する

基本的な内容は 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 で下記のようなフォームを表示するのに使われているようです。

f:id:mslGt:20190219063847p:plain

試しにコメントアウトしてみると、他と同じようにただ入力エリアが表示されていました。

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 つがあります。

  1. DbContext
  2. DbContextOptions
  3. 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 (DbContextOptions options)
            : base(options)
        {
        }
        public DbSet Movie { get; set; }        
    }
}

このチュートリアルでは、既存の DB を使うのではなくコードから生成します。

その名前は appsettings.json で指定しています。

appsettings.json

{
~省略~
  "ConnectionStrings": {
    "MovieContext": "Data Source=MvcMovie.db",
  }
}

必要なパッケージを NuGet でインストールします(どれもバージョンは 2.2.2 にしました)。

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 Task Index()
        {
            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

CUIGUI より最初とっつきにくい印象があるものの覚えた後は楽なので(個人の感想です)、どちらで慣れるのが良いのかは決めづらいどころですが。

これでとりあえず 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 だとどのようにマズいのかを見てみることにします。

※とはいいつつググるといくらでも出てくるんですけど。

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 というクラスが一括で投げているようです。

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/ThrowHelper.cs

例えば 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() に渡しているようです。

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Runtime/ExceptionServices/ExceptionDispatchInfo.cs

で、ここから若干ややこしいのですが、この TrySetException() というのは Task< TResult> クラスで定義されているのですが、このクラスは Future.cs にあります。

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Threading/Tasks/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 を追加しています。

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Threading/Tasks/TaskExceptionHolder.cs

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 として投げるためのイベント、ということのようです。

この辺りの Exception の扱いの違いが、結果の違いとなって現れているようですね。

AggregateException とは

Wait() や WaitAll() で使われていた、 AggregateException について少しだけ。

Microsoft Docsによると、アプリケーションの実行中に発生した、 1 つ以上のエラーを表す、とのこと。

なるほどわからん

この辺りの説明を読むと、 複数の Task がそれぞれ非同期で Exception を出しうる場合に、それらを一括で扱えるようにするために AggregateException が使われている、ということのようです。

今回の例では Wait() は単体の Task で使いましたが、親子関係を持たせた場合にこれが必要になるようです。

await では親子関係は持てないの?という疑問はありますが、この辺りは次回以降試してみたいと思います。

なお、 AggregateException が返る場合、 InnerExceptions で個別の Exception を取り出せたり、 AggregateException に親子関係がある場合も Flatten() を使ってまとめて取り出せたりするため便利ですね。

ということで雑に Exception でキャッチせず、ちゃんと AggregateException を指定しましょう(戒め)。

参照

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::AwaitUnsafeOnCompletedd__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::Startd__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# の複数バージョンに対応するため?に省かれたっポイですが。

なので細かいところはそちらを参照、というところなのですが、乱雑にまとめますと

  1. await を使うと IAsyncStateMachine を継承した構造体が作られる
    (リンク先を見ると StateMachine という名前らしいです)
  2. 非同期メソッドが完了したかなどの状態は TaskAwaiter が持つ
  3. 1.が AsyncTaskMethodBuilder を使った非同期メソッドの生成や 2.の登録・監視などを行う

といった感じでしょうか。

TaskAwaiter はコードを見ると interface を継承していますが、中身は空のようでした。

また、 await で処理を待ち受けることができる型は、 Awaitable パターンに沿っていること、という制限があるようです。

この Awaitable パターンというのは特定の型を指すのではなく、 GetAwaiter() など処理に必要なメソッドが実装されていれば良い、ということのようです。

動的な型を持つ言語や Go の interface もこんな感じでしたね。

この辺りで発生するエラーをキャッチするために try - catch があるのかもしれませんね。

IL のコードを載せたために長くなってしまったので、ここで切ります。

参照