{"id":21183380,"url":"https://github.com/koenbeuk/entityframeworkcore.triggered","last_synced_at":"2025-05-14T04:07:47.750Z","repository":{"id":37433331,"uuid":"279947632","full_name":"koenbeuk/EntityFrameworkCore.Triggered","owner":"koenbeuk","description":"Triggers for EFCore. Respond to changes in your DbContext before and after they are committed to the database.","archived":false,"fork":false,"pushed_at":"2024-12-10T04:34:50.000Z","size":1631,"stargazers_count":579,"open_issues_count":4,"forks_count":29,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-05-11T10:05:43.678Z","etag":null,"topics":["dotnet","efcore","entity-framework","entity-framework-core","sql","trigger"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/koenbeuk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-07-15T18:33:10.000Z","updated_at":"2025-05-02T14:57:01.000Z","dependencies_parsed_at":"2024-12-19T00:01:44.447Z","dependency_job_id":"6ab32d98-824b-40df-906e-b71a4157894e","html_url":"https://github.com/koenbeuk/EntityFrameworkCore.Triggered","commit_stats":{"total_commits":426,"total_committers":9,"mean_commits":"47.333333333333336","dds":0.2300469483568075,"last_synced_commit":"fc0ca32505c99a790998a7951e714efd6ab08867"},"previous_names":[],"tags_count":50,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/koenbeuk%2FEntityFrameworkCore.Triggered","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/koenbeuk%2FEntityFrameworkCore.Triggered/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/koenbeuk%2FEntityFrameworkCore.Triggered/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/koenbeuk%2FEntityFrameworkCore.Triggered/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/koenbeuk","download_url":"https://codeload.github.com/koenbeuk/EntityFrameworkCore.Triggered/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254069599,"owners_count":22009558,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dotnet","efcore","entity-framework","entity-framework-core","sql","trigger"],"created_at":"2024-11-20T18:00:13.706Z","updated_at":"2025-05-14T04:07:47.697Z","avatar_url":"https://github.com/koenbeuk.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# EntityFrameworkCore.Triggered 👿\nTriggers for EF Core. Respond to changes in your DbContext before and after they are committed to the database.\n\n[![NuGet version (EntityFrameworkCore.Triggered)](https://img.shields.io/nuget/v/EntityFrameworkCore.Triggered.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Triggered/)\n[![Build status](https://github.com/koenbeuk/EntityFrameworkCore.Triggered/workflows/.NET%20Core/badge.svg)](https://github.com/koenbeuk/EntityFrameworkCore.Triggered/actions?query=workflow%3A%22.NET+Core%22)\n\n## NuGet packages\n- EntityFrameworkCore.Triggered [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Triggered.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Triggered/) [![NuGet](https://img.shields.io/nuget/dt/EntityFrameworkCore.Triggered.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Triggered/)\n- EntityFrameworkCore.Triggered.Abstractions [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Triggered.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.Abstractions/) [![NuGet](https://img.shields.io/nuget/dt/EntityFrameworkCore.Triggered.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.Abstractions/)\n- EntityFrameworkCore.Triggered.Transactions [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Triggered.Transactions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.Transactions/) [![NuGet](https://img.shields.io/nuget/dt/EntityFrameworkCore.Triggered.Transactions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.Transactions/)\n- EntityFrameworkCore.Triggered.Transactions.Abstractions [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Triggered.Transactions.Abstractions?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.Transactions.Abstractions/) [![NuGet](https://img.shields.io/nuget/dt/EntityFrameworkCore.Triggered.Transactions.Abstractions?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.Transactions.Abstractions/)\n- EntityFrameworkCore.Triggered.Extensions [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Triggered.Extensions?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.Extensions/) [![NuGet](https://img.shields.io/nuget/dt/EntityFrameworkCore.Triggered.Extensions?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.Extensions/)\n\n## Getting started\n1. Install the package from [NuGet](https://www.nuget.org/packages/EntityFrameworkCore.Triggered)\n2. Write triggers by implementing `IBeforeSaveTrigger\u003cTEntity\u003e` and `IAfterSaveTrigger\u003cTEntity\u003e`\n3. Register your triggers with your DbContext\n4. View our [samples](https://github.com/koenbeuk/EntityFrameworkCore.Triggered/tree/master/samples) and [more samples](https://github.com/koenbeuk/EntityFrameworkCore.Triggered.Samples) and [a sample application](https://github.com/koenbeuk/EntityFrameworkCore.BookStoreSampleApp)\n5. Check out our [wiki](https://github.com/koenbeuk/EntityFrameworkCore.Triggered/wiki) for tips and tricks on getting started and being successful.\n\n### Example\n```csharp\nclass StudentSignupTrigger  : IBeforeSaveTrigger\u003cStudent\u003e {\n    readonly ApplicationDbContext _applicationDbContext;\n    \n    public class StudentTrigger(ApplicationDbContext applicationDbContext) {\n        _applicationDbContext = applicationDbContext;\n    }\n\n    public Task BeforeSave(ITriggerContext\u003cStudent\u003e context, CancellationToken cancellationToken) {   \n        if (context.ChangeType == ChangeType.Added){\n            _applicationDbContext.Emails.Add(new Email {\n                Student = context.Entity, \n                Title = \"Welcome!\";,\n                Body = \"....\"\n            });\n        } \n\n        return Task.CompletedTask;\n    }\n}\n\nclass SendEmailTrigger : IAfterSaveTrigger\u003cEmail\u003e {\n    readonly IEmailService _emailService;\n    readonly ApplicationDbContext _applicationDbContext;\n    public StudentTrigger (ApplicationDbContext applicationDbContext, IEmailService emailservice) {\n        _applicationDbContext = applicationDbContext;\n        _emailService = emailService;\n    }\n\n    public async Task AfterSave(ITriggerContext\u003cStudent\u003e context, CancellationToken cancellationToken) {\n        if (context.Entity.SentDate == null \u0026\u0026 context.ChangeType != ChangeType.Deleted) {\n            await _emailService.Send(context.Enity);\n            context.Entity.SentDate = DateTime.Now;\n\n            await _applicationContext.SaveChangesAsync();\n        }\n    }\n}\n\npublic class Student {\n    public int Id { get; set; }\n    public string Name { get; set; }\n    public string EmailAddress { get; set;}\n}\n\npublic class Email { \n    public int Id { get; set; }  \n    public Student Student { get; set; } \n    public DateTime? SentDate { get; set; }\n}\n\npublic class ApplicationDbContext : DbContext {\n    public DbSet\u003cStudent\u003e Students { get; set; }\n    public DbSet\u003cEmail\u003e Emails { get; set; }\n}\n\npublic class Startup\n{\n    public void ConfigureServices(IServiceCollection services)\n    {\n        services.AddSingleton\u003cEmailService\u003e();\n        services\n            .AddDbContext\u003cApplicationContext\u003e(options =\u003e {\n                options.UseTriggers(triggerOptions =\u003e {\n                    triggerOptions.AddTrigger\u003cStudentSignupTrigger\u003e();\n                    triggerOptions.AddTrigger\u003cSendEmailTrigger\u003e();\n                });\n            })\n            .AddTransient\u003cIEmailService, MyEmailService\u003e();\n    }\n\n    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)\n    { ... }\n}\n```\n\n### Related articles\n[Triggers for Entity Framework Core](https://onthedrift.com/posts/efcore-triggered-part1/) - Introduces the idea of using EF Core triggers in your codebase\n\n[Youtube presentation](https://youtu.be/Gjys0Yebobk?t=671) - Interview by the EF Core team\n\n### Trigger discovery\nIn the given example, we register triggers directly with our DbContext. This is the recommended approach starting from version 2.3 and 1.4 respectively. If you're on an older version then it's recommended to register triggers with your application's DI container instead:\n\n```csharp\n    services\n        .AddDbContext\u003cApplicationContext\u003e(options =\u003e options.UseTriggers())\n        .AddTransient\u003cIBeforeSaveTrigger\u003cUser\u003e, MyBeforeSaveTrigger\u003cUser\u003e\u003e();\n```\n\nDoing so will make sure that your triggers can use other services registered in your DI container.\n\nYou can also use functionality in [EntityFrameworkCore.Triggered.Extensions](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.Extensions/) which allows you to discover triggers that are part of an assembly: \n\n```csharp\nservices.AddDbContext\u003cApplicationContext\u003e(options =\u003e options.UseTriggers(triggerOptions =\u003e triggerOptions.AddAssemblyTriggers()));\n// or alternatively\nservices.AddAssemblyTriggers();\n```\n\n### DbContext pooling\nWhen using EF Core's [DbContext pooling](https://docs.microsoft.com/en-us/ef/core/performance/advanced-performance-topics?tabs=with-constant#dbcontext-pooling), Triggers internally needs to discover the `IServiceProvider` that was used to obtain a lease on the current DbContext. Thankfully, all that's complexity is hidden and all that's required is a call to `AddTriggeredDbContextPool`.\n\n```csharp\nservices.AddDbContextPool\u003cApplicationDbContext\u003e(...); // Before\nservices.AddTriggeredDbContextPool\u003cApplicationDbContext\u003e(...); // After\n```\n\n### Cascading changes (previously called Recursion)\n`BeforeSaveTrigger\u003cTEntity\u003e` supports cascading triggers. This is useful since it allows your triggers to subsequently modify the same DbContext entity graph and have it raise additional triggers. By default this behavior is turned on and protected from infinite loops by limiting the number of cascading cycles. If you don't like this behavior or want to change it, you can do so by:\n```csharp\noptionsBuilder.UseTriggers(triggerOptions =\u003e {\n    triggerOptions.CascadeBehavior(CascadeBehavior.EntityAndType).MaxRecusion(20)\n})\n```\n\nCurrently there are 2 types of cascading strategies out of the box: `NoCascade` and `EntityAndType` (default). The former simply disables cascading, whereas the latter cascades triggers for as long as the combination of the Entity and the change type is unique. `EntityAndType` is the recommended and default cascading strategy. You can also provide your own implemention.\n\n### Inheritance\nTriggers support inheritance and sort execution of these triggers based on least concrete to most concrete. Given the following example:\n```csharp\ninterface IAnimal { }\nclass Animal : IAnimal { }\ninterface ICat : IAnimal { }\nclass Cat : Animal, ICat { }\n```\n\nIn this example, triggers will be executed in the order: \n* those for `IAnimal`, \n* those for `Animal`\n* those for `ICat`, and finally \n* `Cat` itself. \n\nIf multiple triggers are registered for the same type, they will execute in order they were registered with the DI container.\n\n### Priorities\nIn addition to inheritance and the order in which triggers are registered, a trigger can also implement the `ITriggerPriority` interface. This allows a trigger to configure a custom priority (default: 0). Triggers will then be executed in order of their priority (lower goes first). This means that a trigger for `Cat` can execute before a trigger for `Animal`, for as long as its priority is set to run earlier. A convenient set of priorities are exposed in the `CommonTriggerPriority` class.\n\n### Error handling\nIn some cases, you want to be triggered when a `DbUpdateException` occurs. For this purpose we have `IAfterSaveFailedTrigger\u003cTEntity\u003e`. This gets triggered for all entities as part of the change set when DbContext.SaveChanges raises a DbUpdateException. The handling method: `AfterSaveFailed` in turn gets called with the trigger context containing the entity as well as the exception. You may attempt to call `DbContext.SaveChanges` again from within this trigger. This will not raise triggers that are already raised and only raise triggers that have since become relevant (based on the cascading configuration). \n\n### Lifecycle triggers\nStarting with version 2.1.0, we added support for \"Lifecycle triggers\". These triggers are invoked once per trigger type per `SaveChanges` lifecyle and reside within the   `EntityFrameworkCore.Triggered.Lifecycles` namespace. These can be used to run something before/after all individual triggers have run. Consider the following example:\n```csharp\npublic BulkReportTrigger : IAfterSaveTrigger\u003cEmail\u003e, IAfterSaveCompletedTrigger {\n    private List\u003cstring\u003e _emailAddresses = new List\u003cstring\u003e();\n    \n    // This may be invoked multiple times within the same SaveChanges call if there are multiple emails\n    public Task AfterSave(ITriggerContext\u003cEmail\u003e context, CancellationToken cancellationToken) { \n        if (context.ChangeType == ChangeType.Added) { \n            this._emailAddresses.Add(context.Address);\n            return Task.CompletedTask;\n        }\n    }\n    \n    public Task AfterSaveCompleted(CancellationToken cancellationToken) {\n        Console.WriteLine($\"We've sent {_emailAddresses.Count()} emails to {_emailAddresses.Distinct().Count()}\" distinct email addresses\");\n        return Task.CompletedTask;\n    }\n}\n```\n\n### Transactions\nMany database providers support the concept of a Transaction. By default when using SqlServer with EntityFrameworkCore, any call to `SaveChanges` will be wrapped in a transaction. Any changes made in `IBeforeSaveTrigger\u003cTEntity\u003e` will be included within the transaction and changes made in `IAfterSaveTrigger\u003cTEntity\u003e` will not. However, it is possible for the user to [explicitly control transactions](https://docs.microsoft.com/en-us/ef/core/saving/transactions). Triggers are extensible and one such extension are [Transactional Triggers](https://www.nuget.org/packages/EntityFrameworkCore.Triggered.Transactions/). In order to use this plugin you will have to implement a few steps:\n```csharp\n// OPTIONAL: Enable transactions when configuring triggers (Required ONLY when not using dependency injection)\ntriggerOptions.UseTransactionTriggers();\n...\nusing var tx = context.Database.BeginTransaction();\nvar triggerService = context.GetService\u003cITriggerService\u003e(); // ITriggerService is responsible for creating now trigger sessions (see below)\nvar triggerSession = triggerService.CreateSession(context); // A trigger session keeps track of all changes that are relevant within that session. e.g. RaiseAfterSaveTriggers will only raise triggers on changes it discovered within this session (through RaiseBeforeSaveTriggers)\n\ntry {\n    await context.SaveChangesAsync();\n    await triggerSession.RaiseBeforeCommitTriggers();    \n    await context.CommitAsync();\n    await triggerSession.RaiseAfterCommitTriggers();\n}\ncatch {\n    await triggerSession.RaiseBeforeRollbackTriggers();\n    await context.RollbackAsync();\n    await triggerSession.RaiseAfterRollbackTriggers();\t\n    throw;\n}\n```\nIn this example we were not able to inherit from TriggeredDbContext since we want to manually control the TriggerSession\n\n### Custom trigger types\nBy default we offer 3 trigger types: `IBeforeSaveTrigger`, `IAfterSaveTrigger` and `IAfterSaveFailedTrigger`. These will cover most cases. In addition we offer `IRaiseBeforeCommitTrigger` and `IRaiseAfterCommitTrigger` as an extension to further enhance your control of when triggers should run. We also offer support for custom triggers. Let's say we want to react to specific events happening in your context. We can do so by creating a new interface `IThisThingJustHappenedTrigger` and implementing an extension method for `ITriggerSession` to invoke triggers of that type. Please take a look at how [Transactional triggers](https://github.com/koenbeuk/EntityFrameworkCore.Triggered/tree/master/src/EntityFrameworkCore.Triggered.Transactions) are implemented as an example.\n\n### Async triggers\nAsync triggers are fully supported, though you should be aware that if they are fired as a result of a call to the synchronous `SaveChanges` on your DbContext, the triggers will be invoked and the results waited for by blocking the caller thread as discussed [here](https://github.com/koenbeuk/EntityFrameworkCore.Triggered/issues/127). This is known as the sync-over-async problem which can result in deadlocks. It's recommended to use `SaveChangesAsync` to avoid the potential for deadlocks, which is also best practice anyway for an operation that involves network/file access as is the case with an EF Core read/write to the database.\n\n\u003e Starting from v4 (currently in preview), we offer both synchronous and asynchronous triggers.\n\n### Versions\n- [V4-preview](https://www.nuget.org/packages/EntityFrameworkCore.Triggered/4.0.0) is an evolution of this library with breaking changes. This is still in production and not ready for production use.\n- [V3](https://www.nuget.org/packages/EntityFrameworkCore.Triggered/3.2.2) is available for those targeting EF Core 6 or later and is the current stable offering.\n- [V2](https://www.nuget.org/packages/EntityFrameworkCore.Triggered/1.4.0) Targets EF Core 3.1. This requires you to inherit from `TriggeredDbContext` or handle manual management of trigger sessions.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkoenbeuk%2Fentityframeworkcore.triggered","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkoenbeuk%2Fentityframeworkcore.triggered","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkoenbeuk%2Fentityframeworkcore.triggered/lists"}