1

I needed to handle async events safely without the risk of "implicit parallelism", and so implemented the "deferral" mechanism advised by @StephenCleary, using his Nito.AsyncEx / Nito.AsyncEx.Oop library and great tutorial on async events.

It works well. So my event subscriber looks like this:

dbContext.SavingChanges += async (sender, e) => {
  using (e.GetDeferral()) {
    Audit();
    await Validate();   // this could throw
  }
};

However, suppose that Validate() throws an exception. I need it to "bubble up" to the event's producer. In other words, since this is a "command event", which updates the event producer when it's done, I'd like the exception to also reach there. But it doesn't of course.

Is there a way to do that?

(Background: just before the db context saves, it raises events for handlers to do extra work, e.g. auditing, validation. If validation fails, I want the context to catch the exception so it can abort the save.)

grokky
  • 8,537
  • 20
  • 62
  • 96
  • 3
    The design of events within .NET doesn't really work well here - events are meant to be subscribable by multiple handlers. Those handlers need to do their own error handling since what does it "mean" if some handlers process successfully and then one of them throws? – Damien_The_Unbeliever Apr 12 '17 at 10:09
  • I agree, but with "command style" events, one must take exceptions into account. Since there is a signalling back to the source, there must be some way to send back a payload as well (the payload being an exception, which can be rethrown). – grokky Apr 12 '17 at 10:19
  • @Damien_The_Unbeliever Yeah you're right, that specific point is what made me rethink the design... if you have multiple return values or exceptions, which one do you rethrow/process? Kind of arbitrary, so it's a poor design. – grokky Apr 12 '17 at 15:31

1 Answers1

1

The core problem is due to what I call "command events". These aren't a natural fit because they're implementing the Strategy pattern by using events, and events are intended to implement the Observer pattern. It's this design-level mismatch that make "command events" difficult to work with.

The deferral pattern is my first choice for "command events", since it's very similar to the deferral pattern used in UAP apps. However, it does assume that the events are truly events - that is, async void methods that logically operate at the "entry level" of an app. Deferrals allow you to detect when they complete; it does not work to propagate exceptions. Exceptions (and any other kinds of results) are not compatible with the Observer pattern (and thus are awkward to implement using events).

Since your application requires more of a Strategy pattern, there are a couple of options. The first is to try to force it into an event implementation (e.g., the "Task-returning delegate solution": make your event type Func<Task>, and have your raising code use Delegate.GetInvocationList and Task.WhenAll or a foreach around await). The disadvantage of this approach is that it forces all your handlers to have an asynchronous signature; synchronous handlers can return Task.CompletedTask, so it's not the end of the world, but it's a bit ugly.

The other approach is to implement the Strategy pattern in the more common way: with interfaces. In your case, you'd need a list of interfaces. The disadvantage of this approach is that it feels like you're re-implementing delegates and events, since you have a list of implementations and the interface/class methods are pretty much just Invoke/InvokeAsync.

Which approach you take is up to you. Both approaches have their own drawbacks.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Another way, which is inelegant but works, is to return a payload in the event's args (the one that contains the deferral manager), which could be a list of values or exceptions, etc. Ugly but in a pinch would work. Would need to be a list of return values so that multiple subscribers won't clobber each other's return values. (Tricky with list of exceptions though, because which one do you throw? :) ) – grokky Apr 12 '17 at 15:24
  • Yeah I think all your points are valid. I'm going to redesign accordingly. Thanks again Stephen. – grokky Apr 12 '17 at 15:24