在EF的DbContext.SaveChangesAsync()中引發異步事件

async-await asynchronous c# entity-framework 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()做到這一點?

一般承認的答案

所以我想提出一個同步或異步事件,以允許偵聽器做額外的工作,阻塞直到它們完成(!),然後調用DbContext基類來實際保存。

如您所見,SaveChanges()很容易

不是真的...... SaveChanges不會等待任何異步處理程序完成。通常,不建議阻止異步工作;即使在ASP.NET Core等環境中你也不會死鎖 ,它確實會影響你的可擴展性。由於MyContext允許異步處理程序,因此您可能希望覆蓋SaveChanges以僅拋出異常。或者,您可以選擇阻止,並希望用戶不會使用異步SaveChanges異步處理程序。

關於實現本身,我在博客文章中描述了一些關於異步事件的方法 。我個人最喜歡的是延遲方法,看起來像這樣(使用我的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
  }
};

熱門答案

我建議修改這個異步事件處理程序

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


Related

許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow