
如何正确使用EntityFrameworkCore.Triggered异步触发器避开sync-over-async死锁陷阱的完整指南【免费下载链接】EntityFrameworkCore.TriggeredTriggers for EFCore. Respond to changes in your DbContext before and after they are committed to the database.项目地址: https://gitcode.com/gh_mirrors/en/EntityFrameworkCore.TriggeredEntityFrameworkCore.Triggered 是一个为 EF Core 添加触发器能力的开源库它让你可以在 DbContext 的改动写入数据库前后执行自定义逻辑。它的**异步触发器async trigger**支持await查询数据库、发送邮件等耗时操作但使用不当会掉进 sync-over-async 死锁陷阱。本文手把手教你正确编写异步触发器、注册触发器并彻底避开死锁。一、EntityFrameworkCore.Triggered 异步触发器能做什么想象一下每当一个新的Student被保存时你希望自动帮他报名所有必修课程。手写if判断散落在各处代码里既繁琐又容易遗漏。异步触发器把这类保存前后自动执行的逻辑集中到独立的触发器类中BeforeSave 异步触发器实体写入数据库前执行可以修改实体、向 DbContext 添加新实体改动会一并入库AfterSave 异步触发器实体成功入库后执行适合发邮件、写审计日志等副作用操作AfterSaveFailed 异步触发器保存失败如数据库约束冲突时执行方便记录异常。官方示例里就有一个典型的 BeforeSave 异步触发器学生新增时自动报名必修课见 samples/2 - PrimarySchool/Triggers/StudentSignupToMandatoryCourses.cs学生报名逻辑以及 samples/3 - StudentManagerAspNetCore/Triggers/Students/SignupToMandatoryCourses.cs完整 Web 版示例。二、异步触发器接口速览IAfterSaveAsyncTrigger 与 IBeforeSaveAsyncTrigger异步触发器接口都定义在EntityFrameworkCore.Triggered.Abstractions包中命名空间为EntityFrameworkCore.Triggered核心接口如下接口执行时机方法签名IBeforeSaveAsyncTriggerTEntity保存前BeforeSaveAsync(context, cancellationToken)IAfterSaveAsyncTriggerTEntity保存后AfterSaveAsync(context, cancellationToken)IAfterSaveFailedAsyncTriggerTEntity保存失败AfterSaveFailedAsync(context, exception, cancellationToken)接口源码非常简洁以 src/EntityFrameworkCore.Triggered.Abstractions/IAfterSaveAsyncTrigger.cs 为例public interface IAfterSaveAsyncTriggerTEntity where TEntity : class { Task AfterSaveAsync(ITriggerContextTEntity context, CancellationToken cancellationToken); }一个真实的异步触发器长这样自动报名必修课触发器内通过await查询数据库public class SignupToMandatoryCourses : IBeforeSaveAsyncTriggerStudent { readonly ApplicationDbContext _applicationContext; public SignupToMandatoryCourses(ApplicationDbContext applicationContext) { _applicationContext applicationContext; } public async Task BeforeSaveAsync(ITriggerContextStudent context, CancellationToken cancellationToken) { // 关键点1触发器内部使用异步数据库查询 var mandatoryCourses await _applicationContext.Courses .Where(x x.IsMandatory) .ToListAsync(cancellationToken); foreach (var course in mandatoryCourses) { if (_applicationContext.StudentCourses.Find(context.Entity.Id, course.Id) null) { _applicationContext.StudentCourses.Add( new StudentCourse { Student context.Entity, Course course }); } } } }ITriggerContextTEntity提供Entity当前实体和ChangeTypeAdded / Modified / Deleted属性帮你判断实体处于什么变更状态。三、最快配置方法三步注册异步触发器注册分三步走第 1 步安装 NuGet 包EntityFrameworkCore.Triggered含核心实现第 2 步在配置DbContext时调用UseTriggers()并注册触发器services.AddDbContextApplicationDbContext(options { options.UseTriggers(triggerOptions { triggerOptions.AddTriggerSignupToMandatoryCourses(); }); });第 3 步像平常一样调用SaveChangesAsync触发器会自动生效。如果你希望自动扫描整个程序集里的所有触发器EntityFrameworkCore.Triggered.Extensions包提供了更省心的方式见 src/EntityFrameworkCore.Triggered.Extensions/ServiceCollectionExtensions.csoptions.UseTriggers(t t.AddAssemblyTriggers());四、sync-over-async 死锁陷阱为什么会卡死你的线程这是全文最重要的一节 ⚠️。问题本质如果你调用的是同步版SaveChanges()但注册了异步触发器库只能阻塞当前线程等待异步触发器执行完成即 sync-over-async。在 Web 等线程池受限的场景下被阻塞的线程一直不释放而异步操作完成后又需要一个线程池线程来继续——线程池被耗尽程序就死锁了。从拦截器源码 src/EntityFrameworkCore.Triggered/Internal/TriggerSessionSaveChangesInterceptor.cs 可以看到同步入口SavingChanges和异步入口SavingChangesAsync是两条完全独立的路径异步入口会真正await触发器_triggerSession.RaiseBeforeSaveTriggers(); await _triggerSession.RaiseBeforeSaveAsyncTriggers(cancellationToken).ConfigureAwait(false);而触发器编排核心 src/EntityFrameworkCore.Triggered/TriggerSession.cs 中的RaiseAsyncTriggers方法才是异步触发器被await执行的真正场所支持取消令牌、按优先级顺序执行。4 条规则一次避开死锁#规则说明1一律使用SaveChangesAsync有异步触发器时必须走异步保存路径让触发器被真正await而非阻塞线程2触发器内禁止.Result/.Wait()这是 sync-over-async 的典型写法会直接引发死锁3触发器内数据库操作用异步 API如ToListAsync(cancellationToken)并透传取消令牌4同步代码路径也要留意若某处必须用同步 API 调用SaveChanges要么改为异步要么把该触发器实现为同步接口IBeforeSaveTriggerT正确的保存调用示例context.Students.Add(newStudent); await context.SaveChangesAsync(ct); // ✅ 异步保存异步触发器被 await 执行 // context.SaveChanges(); // ❌ 会阻塞线程等待异步触发器可能死锁五、进阶技巧让异步触发器更好用取消令牌要透传触发器方法都带有CancellationToken参数请把它传给所有内部await调用让取消请求及时生效控制执行顺序实现ITriggerPriority接口可自定义优先级数值小先执行同类型的多个触发器按注册顺序执行级联触发CascadingBeforeSave 触发器修改实体图后可以再次触发其他触发器。默认开启EntityAndType级联策略并限制最大递归次数防止死循环可通过UseTriggers的CascadeBehavior(...).MaxRecusion(20)调整见 src/EntityFrameworkCore.Triggered/Infrastructure/CascadeBehavior.cs生命周期触发器IBeforeSaveStartingAsyncTrigger、IAfterSaveCompletedAsyncTrigger等接口在整个保存生命周期只执行一次适合做批量统计接口位于 src/EntityFrameworkCore.Triggered.Abstractions/Lifecycles/ 目录保存失败处理用IAfterSaveFailedAsyncTriggerT捕获DbUpdateException在触发器内可尝试修正数据后再次保存Roslyn 分析器帮你迁移v4 起同步/异步触发器彻底分离。仓库自带分析器见 EntityFrameworkCore.Triggered.Analyzers/EntityFrameworkCore.Triggered.Analyzers/TriggerMigrationAnalyzer.cs会扫描你代码中同步接口 Task 方法的旧签名诊断码 EFCT001并提供 CodeFix 一键改写为IBeforeSaveAsyncTriggerT/IAfterSaveAsyncTriggerT等新接口。六、总结3 个要点带走异步触发器接口IBeforeSaveAsyncTriggerT/IAfterSaveAsyncTriggerT让触发器可以await数据库查询和外部服务逻辑集中、不侵入业务代码注册只需一行UseTriggers(...)配合AddAssemblyTriggers()可全自动发现触发器永远用SaveChangesAsync搭配异步触发器——这是避开 sync-over-async 死锁的黄金法则。更多完整用法可参考仓库中的示例项目samples/2 - PrimarySchool/Program.cs 演示了控制台应用中最小化的触发器注册流程。【免费下载链接】EntityFrameworkCore.TriggeredTriggers for EFCore. Respond to changes in your DbContext before and after they are committed to the database.项目地址: https://gitcode.com/gh_mirrors/en/EntityFrameworkCore.Triggered创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考