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. 🙇
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();
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();
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.
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.