Кажется, у вас есть пара вопросов. Давайте разберемся с ними индивидуально
Создание WaitHandle лениво
Да, это самый правильный подход. Вы должны делать это безопасным способом, но путь ленив.
Хитрость заключается в избавлении от WaitHandle. WaitHandle - это база IDisposable, и необходимо своевременно утилизировать . Документация для IAsycResult не охватывает этот случай. Лучший способ сделать это в EndInvoke. В документации для BeginInvoke прямо указано, что для каждого BeginInvoke должен быть соответствующий EndInvoke (или BeginRead / EndRead). Это лучшее место для утилизации WaitHandle.
Как должен быть реализован AsyncState?
Если вы посмотрите на стандартные API BCL, которые возвращают IAsyncResult, большинство из них принимают параметр состояния. Обычно это значение, которое возвращается из AsyncState (пример см. В Socket API). Рекомендуется включать переменную состояния, типизированную как объект, для любого API стиля BeginInvoke, который возвращает IAsyncResult. Не обязательно, но хорошая практика.
При отсутствии переменной состояния возвращаемое значение null является приемлемым.
IsCompleted API
Это будет сильно зависеть от реализации, которая создает IAsyncResult. Но да, вы должны это реализовать.