Reputation: 2203
I have a data repository that provides a persistence layer for the models of my application. All access to the disc is asynchronous so my whole persistence layer is written with async/await. My data repository allows other modules to subscribe to changes in the data:
public event EventHandler<Journaling.DataChangeEventArgs> DataChanged;
protected void OnDataChanged(Journaling.Action a)
{
if (DataChanged != null)
{
DataChanged(this, new Journaling.DataChangeEventArgs(a));
}
}
When I tell the repository to delete an object that is referenced by other objects, it deletes these other objects as well.
public async Task<bool> DeleteAsync(Models.BaseModel model)
{
await DeleteDependentModelsAsync(model).ConfigureAwait(false);
if (await connection.DeleteAsync(model).ConfigureAwait(false) != 1)
return false;
else
{
var deleteAction = new Journaling.DeleteAction(model);
OnDataChanged(deleteAction);
return true;
}
}
This setup works to some extent, but I have a problem when deleting objects that reference other objects. Consider this example:
Object X
Object A1: references X
Object A2: references X
Object A3: references X
I have a logger module that subscribes to changes in the data repository and outputs them to a file. The logger sometimes needs to fetch additional data from the repository to make its output human readable. The log when deleting Object X should be:
A1 deleted (parent: X, more information contained in X)
A2 deleted (parent: X, more information contained in X)
A3 deleted (parent: X, more information contained in X)
X deleted
The problem is that OnDataChange doesn't await the execution of the event handlers. Because of this the data repository deletes A1-A3 and then X before the event handler of the logger is even called once. But the event handler has to fetch some information about X, which isn't possible any more because the data repository has already deleted X.
I somehow have to await the execution of the event handler in OnDataChanged. This way I could make sure that the logger has finished its work before the next object is deleted from storage.
Can anyone hint me in the right direction on how to do this? I have considered using semaphores but this would break the loose coupling I have between the data repository and the logger.
Upvotes: 3
Views: 826
Reputation: 456587
I have a blog post on the subject of "asynchronous events". In general, I recommend using "deferrals", which are a concept from the Windows Store API.
For example, using the DeferralManager
type from my AsyncEx library, you can first enable your event arguments type to support deferrals:
public class DataChangeEventArgs : EventArgs
{
private readonly DeferralManager _deferrals;
public DataChangeEventArgs(DeferralManager deferrals, Journaling.Action a)
{
_deferrals = deferrals;
}
public IDisposable GetDeferral()
{
return deferrals.GetDeferral();
}
}
Then you raise the event as such:
protected Task OnDataChangedAsync(Journaling.Action a)
{
var handler = DataChanged;
if (handler == null)
return Task.FromResult<object>(null); // or TaskConstants.Completed
var deferrals = new DeferralManager();
var args = new Journaling.DataChangeEventArgs(deferrals, a);
handler(args);
return deferrals.SignalAndWaitAsync();
}
Consuming code can then use a deferral if it needs to use await
:
async void DataChangedHandler(object sender, Journaling.DataChangeEventArgs args)
{
using (args.GetDeferral())
{
// Code can safely await in here.
}
}
Upvotes: 4
Reputation: 203842
Since your handlers are asynchronous, and the type invoking those handlers needs to know when they finish, those handlers need to return a Task
instead of being void
.
When invoking this handler you'll need to get the invocation list and invoke each individual method, rather than invoking all at once, because you need to be able to get all of the return values.
You'll also need to alter the signature of OnDataChanged
to return a Task
so that the caller will be able to know when it has finished.
public event Func<Journaling.Action, Task> DataChanged;
protected Task OnDataChanged(Journaling.Action a)
{
var handlers = DataChanged;
if (handlers == null)
return Task.FromResult(0);
var tasks = DataChanged.GetInvocationList()
.Cast<Func<Journaling.Action, Task>>()
.Select(handler => handler(a));
return Task.WhenAll(tasks);
}
Upvotes: 2