Create a Task (or Observable) from legacy class (start method + events)

I have a legacy class which I would like to consume in a more convenient way.

class Camera 
{
     void RequestCapture();
     public delegate void ReadResultEventHandler(ControllerProxy sender, ReadResult readResult);
     public event ReadResultEventHandler OnReadResult;
}

The class manages a camera that takes pictures. It works by:

  1. Invoking the RequestCapture method
  2. Awaiting for the OnReadResult to be raised (obviously, you need to subscribe to this event in order to get the captured data)

The operation is asynchronous. The RequestCapture is a fire-and-forget operation (fast!) operation. After some seconds, the event will raise.

I would like to consume it either like as regular Task or as an IObservable<ReadResult> but I’m quite lost, since it doesn’t adapt to the Async Pattern, and I have never used a class like this.

Answer

Following the example in this article my rudimentary approach (untested!!) would be:

class AsyncCamera
{
    private readonly Camera camera;
    
    public AsyncCamera(Camera camera)
    {
        this.camera = camera ?? throw new ArgumentNullException(nameof(camera));
    } 

    public Task<ReadResult> CaptureAsync()
    {
         TaskCompletionSource<ReadResult> tcs = new TaskCompletionSource<ReadResult>();
         camera.OnReadResult += (sender, result) => {
             tcs.TrySetResult(result);
         };
         camera.RequestCapture();
         return tcs.Task;
    }
}

Possible usage:

public async Task DoSomethingAsync()
{
    var asyncCam = new AsyncCamera(new Camera());
    var readResult = await asyncCam.CaptureAsync();
    // use readResult
}

EDIT taking Stephen’s comment into account (T H A N K you!)

So, this is my naive take on unsubscribing the event handler. I didn’t have the time to make any tests, so suggestions for improvement are welcome.

class AsyncCamera
{
    private readonly Camera camera;
    
    public AsyncCamera(Camera camera)
    {
        this.camera = camera ?? throw new ArgumentNullException(nameof(camera));
    } 

    public Task<ReadResult> CaptureAsync()
    {
         ReadResultEventHandler handler = null;
         TaskCompletionSource<ReadResult> tcs = new TaskCompletionSource<ReadResult>();

         handler = (sender, result) => {
             camera.OnReadResult -= handler;
             tcs.TrySetResult(result);
         };

         camera.OnReadResult += handler;
         camera.RequestCapture();
         return tcs.Task;
    }
}

In case comments get “cleaned up”: Stephen said

Unsubscription isn’t handled in the MS examples, but it really should be. You’d have to declare a variable of type ReadResultDelegate (or whatever it’s called), set it to null, and then set it to the lambda expression which can then unsubscribe that variable from the event. I don’t have an example of this on my blog, but there’s a general-purpose one here. Which, now that I look at it, does not seem to handle cancellation appropriately. – Stephen Cleary

Emphasis by me.

Seems to work: https://dotnetfiddle.net/9XsaUB