Skip to content

Instantly share code, notes, and snippets.

@Swimburger
Last active March 16, 2024 17:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Swimburger/6f81ccbab075422c0e4f73f6d5ac99da to your computer and use it in GitHub Desktop.
Save Swimburger/6f81ccbab075422c0e4f73f6d5ac99da to your computer and use it in GitHub Desktop.
3 options of event implementation for transcriber class

After some recent discussions about the C# events their drawbacks, specifically with async callbacks, I prototyped a couple of alternative event APIs for a Speech-To-Text library that I'm working on.

Please let me know which one you prefer, or better alternatives. 🙇

Option 1

For each event, have an event object to which you can subscribe and unsubscribe.

// disposing transcriber also unsubscribes events
using var transcriber = createTranscriber();
await transcriber.ConnectAsync();

void OnPartialTranscript(PartialTranscript transcript){
    ...  
}

async Task OnPartialTranscriptAsync(PartialTranscript transcript){
    ...  
}

// sync event handler
transcriber.OnPartialTranscript.Subscribe(OnPartialTranscript);

// async task event handler
transcriber.OnPartialTranscript.Subscribe(OnPartialTranscriptAsync);

// unsubscribe from event individual callbacks
transcriber.OnPartialTranscript.Unsubscribe(OnPartialTranscript);
transcriber.OnPartialTranscript.Unsubscribe(OnPartialTranscriptAsync);

// or unsubscribe all
transcriber.OnPartialTranscript.UnsubscribeAll();

Option 2

In addition to option 1, wrap all events in an events class. (We only show one event in the sample, but there are many.)

// disposing transcriber also unsubscribes events
using var transcriber = createTranscriber();
await transcriber.ConnectAsync();

void OnPartialTranscript(PartialTranscript transcript){
    ...  
}

async Task OnPartialTranscriptAsync(PartialTranscript transcript){
    ...  
}

// sync event handler
transcriber.Events.PartialTranscript.Subscribe(OnPartialTranscript);

// async task event handler
transcriber.Events.PartialTranscript.Subscribe(OnPartialTranscriptAsync);

// unsubscribe from event individual callbacks
transcriber.Events.PartialTranscript.Unsubscribe(OnPartialTranscript);
transcriber.Events.PartialTranscript.Unsubscribe(OnPartialTranscriptAsync);

// or unsubscribe all PartialTranscript events
transcriber.Events.PartialTranscript.UnsubscribeAll();

// or unsubscribe all events
transcriber.Events.UnsubscribeAll();

Option 3

Use event methods directly on the transcriber class, without an unsubscribe method. Instead, the event method returns a disposable subscription object. Disposing the subscription unsubscribes the event handler from the event.

// disposing transcriber also unsubscribes events
using var transcriber = createTranscriber();
await transcriber.ConnectAsync();

// sync event handler
using var partialTranscriptSubscription = transcriber.OnPartialTranscript(
    transcript => ...
);

// async task event handler
using var finalTranscriptSubscription = transcriber.OnPartialTranscript(
    async transcript => await Task.FromResult(null)
);

Note

I am aware that some of this is re-implementing the reactive extensions which are great. Unfortunately, every dependency you take as a library author means more friction for the user, and reactive extensions is not bundled with the target framework.

@amoerie
Copy link

amoerie commented Mar 16, 2024

I think option 3 makes the most sense and is the easiest to grasp. With manual unsubscription, you force consumers to ensure correct callback identity, so anonymous lambdas are no longer an option.

@deanward81
Copy link

deanward81 commented Mar 16, 2024

Regarding the note about similarity to Rx - you could still use IObservable<T> and IObserver<T> as they’re part of the framework. Consumers of your lib can add Rx themselves if they want all the fancy bits.

So OnPartialTranscript could be an IObservable<T> and calling Subscribe would pass an IObserver<T>, but you could trivially provide extensions to wrap up the lambda syntax by providing your own impl of IObserver<T> that delegates to the user-provided callback…

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment