tech.guitarrapc.cóm

Technical updates

ConsoleAppFrameworkでasync voidは使えない

ConsoleAppFrameworkでasync voidなコマンドは実行しても正常に完了できません。async Taskを使いましょう。以上です。

どのような挙動になるかを説明しておきます。

ConsoleAppFrameworkのCommand

ConsoleAppFrameworkのコマンドの返り値は次のいずれかを期待しています。エラー時は例外にするならvoidasync Task、ExitCodeを調整したいときはintasync Task<int>を使うという使い分けです。

  • void
  • async Task
  • int
  • async Task<int>

たとえば同期処理しかないコマンドは次のように書けます。

[Command("hello")]
public void Hello(string name)
{
    Console.WriteLine($"Hello {name}!");
}
$ dotnet run ConsoleApp1 -- hello foo
Hello foo!

たとえば非同期処理を含むコマンドなら次のように書けます。

[Command("hello-async")]
public async Task AsyncTask(string name)
{
    var ts = TimeProvider.System.GetTimestamp();
    Console.WriteLine($"Hello {name}!");
    await Task.Delay(1000);
    Console.WriteLine($"Waited {TimeProvider.System.GetElapsedTime(ts)}");
}
$ dotnet run ConsoleApp1 -- hello-async foo
Hello foo!
Waited 00:00:01.0050441

async voidの問題

async Taskと書くつもりで間違えてasync voidにすることはまれに時々あります。1 ではasync voidにしたコマンドはasync Taskとどのように挙動が違うか見てみましょう。

// async Taskをasync voidに変更しただけ
[Command("hello-async-void")]
public async void AsyncVoid(string name)
{
    Console.WriteLine($"Hello {name}!");
    await Task.Delay(1000);
    Console.WriteLine($"Waited");
}

async voidだと非同期処理が完了する前にコマンドが終了してしまいます。このため、await Task.Delay(1000)を待つことなくコマンドがそこで終了するのでWaited...が表示されません。

$ dotnet run ConsoleApp1 -- hello-async-void foo
Hello foo!

IDEやビルド時に気づければよいのですが、5.3.3まではasync voidなコマンドでも何事もなくビルド、実行できるので気づくのに遅れるでしょう。

image

また、デバッグ実行してもawaitの行でステップ実行を進めるとデバッガーが終了します。これはasync voidあるあるですね。

対策

async Taskにしましょう。

また、2025/1/16にリリースされた5.3.4async voidコマンドはアナライザーがエラーを出してビルドできくなりました。2今ConsoleAppFramework v5を使っている人は、このバージョンにアップグレードしておきましょう。

image


  1. voidなコマンドをasyncつけて、というのは頭が動いていない時にうっかりすることはゼロじゃないです。
  2. #157でどのような対応がされたかわかります