在EF的DbContext.SaveChangesAsync()中引发异步事件

时间:2017-04-02 12:23:24

标签: c# entity-framework asynchronous async-await entity-framework-core

我在ASP.NET Core环境中使用EF Core。我的上下文按照请求在我的DI容器中注册。

我需要在上下文的SaveChanges()SaveChangesAsync()之前执行额外的工作,例如验证,审核,调度通知等。其中一些工作是同步的,有些是异步的。

所以我想提出一个同步或异步事件,以允许侦听器做额外的工作,阻塞直到它们完成(!),然后调用DbContext基类来实际保存。

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);
  }

}

正如您所看到的,SaveChanges()很容易,但我如何为SaveChangesAsync()执行此操作?

3 个答案:

答案 0 :(得分:2)

  

所以我想提出一个同步或异步事件,以允许侦听器做额外的工作,阻塞直到它们完成(!),然后调用DbContext基类来实际保存。

     

如您所见,SaveChanges()

很容易

不是...... SaveChanges不会等待任何异步处理程序完成。一般情况下,建议不要阻止异步工作;即使在environments such as ASP.NET Core where you won't deadlock中,它也会影响您的可扩展性。由于您的MyContext允许异步处理程序,因此您可能希望覆盖SaveChanges以仅抛出异常。或者,您可以选择阻止,并希望用户不会过多地使用异步SaveChanges的异步处理程序。

关于实施本身,我在blog post on async events中描述了一些方法。我个人最喜欢的是延迟方法,它看起来像这样(使用我的Nito.AsyncEx.Oop库):

public class MyEventArgs: EventArgs, IDeferralSource
{
  internal DeferralManager DeferralManager { get; } = new DeferralManager();
  public IDisposable GetDeferral() => DeferralManager.DeferralSource.GetDeferral();
}

public class MyContext : DbContext
{
  public event EventHandler<MyEventArgs> SavingChanges;

  public override int SaveChanges(bool acceptAllChangesOnSuccess)
  {
    // You must decide to either throw or block here (see above).

    // Example code for blocking.
    var args = new MyEventArgs();
    SavingChanges?.Invoke(this, args);
    args.DeferralManager.WaitForDeferralsAsync().GetAwaiter().GetResult();

    return base.SaveChanges(acceptAllChangesOnSuccess);
  }

  public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
  {
    var args = new MyEventArgs();
    SavingChanges?.Invoke(this, args);
    await args.DeferralManager.WaitForDeferralsAsync();

    return await base.SaveChangesAsync(acceptAllChangesOnSuccess,  cancellationToken);
  }
}

// Usage (synchronous handler):
myContext.SavingChanges += (sender, e) =>
{
  Thread.Sleep(1000); // Synchronous code
};

// Usage (asynchronous handler):
myContext.SavingChanges += async (sender, e) =>
{
  using (e.GetDeferral())
  {
    await Task.Delay(1000); // Asynchronous code
  }
};

答案 1 :(得分:1)

我建议修改此async event handler

public AsyncEvent SavingChangesAsync;

使用

  // async save
  public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
  {
    await SavingChangesAsync?.InvokeAsync(cancellationToken);
    return await base.SaveChangesAsync(acceptAllChangesOnSuccess,  cancellationToken);
  }

,其中

public class AsyncEvent
{
    private readonly List<Func<CancellationToken, Task>> invocationList;
    private readonly object locker;

    private AsyncEvent()
    {
        invocationList = new List<Func<CancellationToken, Task>>();
        locker = new object();
    }

    public static AsyncEvent operator +(
        AsyncEvent e, Func<CancellationToken, Task> callback)
    {
        if (callback == null) throw new NullReferenceException("callback is null");

        //Note: Thread safety issue- if two threads register to the same event (on the first time, i.e when it is null)
        //they could get a different instance, so whoever was first will be overridden.
        //A solution for that would be to switch to a public constructor and use it, but then we'll 'lose' the similar syntax to c# events             
        if (e == null) e = new AsyncEvent();

        lock (e.locker)
        {
            e.invocationList.Add(callback);
        }
        return e;
    }

    public static AsyncEvent operator -(
        AsyncEvent e, Func<CancellationToken, Task> callback)
    {
        if (callback == null) throw new NullReferenceException("callback is null");
        if (e == null) return null;

        lock (e.locker)
        {
            e.invocationList.Remove(callback);
        }
        return e;
    }

    public async Task InvokeAsync(CancellationToken cancellation)
    {
        List<Func<CancellationToken, Task>> tmpInvocationList;
        lock (locker)
        {
            tmpInvocationList = new List<Func<CancellationToken, Task>>(invocationList);
        }

        foreach (var callback in tmpInvocationList)
        {
            //Assuming we want a serial invocation, for a parallel invocation we can use Task.WhenAll instead
            await callback(cancellation);
        }
    }
}

答案 2 :(得分:1)

有一种更简单的方法(基于on this)。

声明一个返回Task

的多播委托
namespace MyProject
{
  public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e);
}

更新上下文(我只显示异步内容,因为同步内容不变):

public class MyContext : DbContext
{

  public event AsyncEventHandler<EventArgs> SavingChangesAsync;

  public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
  {
    var delegates = SavingChangesAsync;
    if (delegates != null)
    {
      var tasks = delegates
        .GetInvocationList()
        .Select(d => ((AsyncEventHandler<EventArgs>)d)(this, EventArgs.Empty))
        .ToList();
      await Task.WhenAll(tasks);
    }
    return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
  }

}

调用代码如下所示:

context.SavingChanges += OnContextSavingChanges;
context.SavingChangesAsync += OnContextSavingChangesAsync;

public void OnContextSavingChanges(object sender, EventArgs e)
{
  someSyncMethod();
}

public async Task OnContextSavingChangesAsync(object sender, EventArgs e)
{
  await someAsyncMethod();
}

我不确定这是否是100%安全的方法。异步事件很棘手。我测试了多个订阅者,并且它有效。我的环境是ASP.NET Core,所以我不知道它是否适用于其他地方。

我不知道它与其他解决方案的比较,或者哪个更好,但这个更简单,对我来说更有意义。

编辑:如果您的处理程序未更改共享状态,则此方法很有效。如果确实如此,请参阅@stephencleary上面更强大的方法