I'm using EF Core, in an ASP.NET Core environment. My context is registered in my DI container as per-request.
I need to perform extra work before the context's SaveChanges()
or SaveChangesAsync()
, such as validation, auditing, dispatching notifications, etc. Some of that work is sync, and some is async.
So I want to raise a sync or async event to allow listeners do extra work, block until they are done (!), and then call the DbContext
base class to actually save.
public class MyContext : DbContext
{
// sync: ------------------------------
// define sync event handler
public event EventHandler<EventArgs> SavingChanges;
// sync save
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
// raise event for sync handlers to do work BEFORE the save
var handler = SavingChanges;
if (handler != null)
handler(this, EventArgs.Empty);
// all work done, now save
return base.SaveChanges(acceptAllChangesOnSuccess);
}
// async: ------------------------------
// define async event handler
//public event /* ??? */ SavingChangesAsync;
// async save
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
// raise event for async handlers to do work BEFORE the save (block until they are done!)
//await ???
// all work done, now save
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
}
As you can see, it's easy for SaveChanges()
, but how do I do it for SaveChangesAsync()
?
I'd suggest a modification of this async event handler
usage
where
Not really...
SaveChanges
won't wait for any asynchronous handlers to complete. In general, blocking on async work isn't recommended; even in environments such as ASP.NET Core where you won't deadlock, it does impact your scalability. Since yourMyContext
allows asynchronous handlers, you'd probably want to overrideSaveChanges
to just throw an exception. Or, you could choose to just block, and hope that users won't use asynchronous handlers with synchronousSaveChanges
too much.Regarding the implementation itself, there are a few approaches that I describe in my blog post on async events. My personal favorite is the deferral approach, which looks like this (using my
Nito.AsyncEx.Oop
library):There is a simpler way (based on this).
Declare a multicast delegate which returns a
Task
:Update the context (I'm only showing async stuff, because sync stuff is unchanged):
The calling code looks like this:
I'm not sure if this is a 100% safe way to do this. Async events are tricky. I tested with multiple subscribers, and it worked. My environment is ASP.NET Core, so I don't know if it works elsewhere.
I don't know how it compares with the other solution, or which is better, but this one is simpler and makes more sense to me.
EDIT: this works well if your handler doesn't change shared state. If it does, see the much more robust approach by @stephencleary above